Build system

This commit is contained in:
Isaac 2025-05-05 17:58:19 +02:00
parent 058a5297ea
commit 65a0b41071
7 changed files with 762 additions and 1 deletions

View File

@ -1 +1,2 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
spm-files

1
.gitignore vendored
View File

@ -73,3 +73,4 @@ buildServer.json
.build/** .build/**
Telegram.LSP.json Telegram.LSP.json
**/.build/** **/.build/**
spm-files

View File

@ -11,5 +11,8 @@
}, },
"search.exclude": { "search.exclude": {
".git/**": true ".git/**": true
},
"files.associations": {
"memory": "cpp"
} }
} }

View File

@ -393,6 +393,58 @@ class BazelCommandLine:
print(subprocess.list2cmdline(combined_arguments)) print(subprocess.list2cmdline(combined_arguments))
call_executable(combined_arguments) call_executable(combined_arguments)
def get_spm_aspect_invocation(self):
combined_arguments = [
self.build_environment.bazel_path
]
combined_arguments += self.get_startup_bazel_arguments()
combined_arguments += ['build']
if self.custom_target is not None:
combined_arguments += [self.custom_target]
else:
combined_arguments += ['Telegram/Telegram']
if self.continue_on_error:
combined_arguments += ['--keep_going']
if self.show_actions:
combined_arguments += ['--subcommands']
if self.enable_sandbox:
combined_arguments += ['--spawn_strategy=sandboxed']
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
if self.configuration_path is None:
raise Exception('configuration_path is not defined')
combined_arguments += [
'--override_repository=build_configuration={}'.format(self.configuration_path)
]
combined_arguments += self.common_args
combined_arguments += self.common_build_args
combined_arguments += self.get_define_arguments()
combined_arguments += self.get_additional_build_arguments()
if self.remote_cache is not None:
combined_arguments += [
'--remote_cache={}'.format(self.remote_cache),
'--experimental_remote_downloader={}'.format(self.remote_cache)
]
elif self.cache_dir is not None:
combined_arguments += [
'--disk_cache={path}'.format(path=self.cache_dir)
]
combined_arguments += self.configuration_args
combined_arguments += ['--aspects', '//build-system/bazel-utils:spm.bzl%spm_text_aspect']
print(subprocess.list2cmdline(combined_arguments))
call_executable(combined_arguments)
def clean(bazel, arguments): def clean(bazel, arguments):
bazel_command_line = BazelCommandLine( bazel_command_line = BazelCommandLine(
@ -696,6 +748,36 @@ def query(bazel, arguments):
bazel_command_line.invoke_query(query_args) bazel_command_line.invoke_query(query_args)
def get_spm_aspect_invocation(bazel, arguments):
bazel_command_line = BazelCommandLine(
bazel=bazel,
override_bazel_version=arguments.overrideBazelVersion,
override_xcode_version=arguments.overrideXcodeVersion,
bazel_user_root=arguments.bazelUserRoot
)
if arguments.cacheDir is not None:
bazel_command_line.add_cache_dir(arguments.cacheDir)
elif arguments.cacheHost is not None:
bazel_command_line.add_remote_cache(arguments.cacheHost)
resolve_configuration(
base_path=os.getcwd(),
bazel_command_line=bazel_command_line,
arguments=arguments,
additional_codesigning_output_path=None
)
bazel_command_line.set_configuration(arguments.configuration)
bazel_command_line.set_build_number(arguments.buildNumber)
bazel_command_line.set_custom_target(arguments.target)
bazel_command_line.set_continue_on_error(False)
bazel_command_line.set_show_actions(False)
bazel_command_line.set_enable_sandbox(False)
bazel_command_line.set_split_swiftmodules(False)
bazel_command_line.get_spm_aspect_invocation()
def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser): def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser):
configuration_group = current_parser.add_mutually_exclusive_group(required=True) configuration_group = current_parser.add_mutually_exclusive_group(required=True)
configuration_group.add_argument( configuration_group.add_argument(
@ -1121,6 +1203,38 @@ if __name__ == '__main__':
metavar='query_string' metavar='query_string'
) )
spm_parser = subparsers.add_parser('spm', help='Generate SPM package')
spm_parser.add_argument(
'--target',
type=str,
help='A custom bazel target name to build.',
metavar='target_name'
)
spm_parser.add_argument(
'--buildNumber',
required=False,
type=int,
default=10000,
help='Build number.',
metavar='number'
)
spm_parser.add_argument(
'--configuration',
choices=[
'debug_universal',
'debug_arm64',
'debug_armv7',
'debug_sim_arm64',
'release_sim_arm64',
'release_arm64',
'release_armv7',
'release_universal'
],
required=True,
help='Build configuration'
)
add_codesigning_common_arguments(spm_parser)
if len(sys.argv) < 2: if len(sys.argv) < 2:
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
@ -1229,6 +1343,8 @@ if __name__ == '__main__':
test(bazel=bazel_path, arguments=args) test(bazel=bazel_path, arguments=args)
elif args.commandName == 'query': elif args.commandName == 'query':
query(bazel=bazel_path, arguments=args) query(bazel=bazel_path, arguments=args)
elif args.commandName == 'spm':
get_spm_aspect_invocation(bazel=bazel_path, arguments=args)
else: else:
raise Exception('Unknown command') raise Exception('Unknown command')
except KeyboardInterrupt: except KeyboardInterrupt:

@ -1 +1 @@
Subproject commit 44b6f046d95b84933c1149fbf7f9d81fd4e32020 Subproject commit 41929acc4c7c1da973c77871d0375207b9d0806f

View File

@ -0,0 +1,447 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "SwiftInfo")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//lib:dicts.bzl", "dicts")
# Define provider to propagate data
SPMModulesInfo = provider(
fields = {
"modules": "Dictionary of module information",
"transitive_sources": "Depset of all transitive source files",
}
)
_IGNORE_CC_LIBRARY_ATTRS = [
"data",
"applicable_licenses",
"alwayslink",
"aspect_hints",
"compatible_with",
"deprecation",
"exec_compatible_with",
"exec_properties",
"expect_failure",
"features",
"generator_function",
"generator_location",
"generator_name",
"generator_platform",
"generator_script",
"generator_tool",
"generator_toolchain",
"generator_toolchain_type",
"licenses",
"linkstamp",
"linkstatic",
"name",
"restricted_to",
"tags",
"target_compatible_with",
"testonly",
"to_json",
"to_proto",
"toolchains",
"transitive_configs",
"visibility",
"win_def_file",
"linkopts",
]
_IGNORE_CC_LIBRARY_EMPTY_ATTRS = [
"additional_compiler_inputs",
"additional_linker_inputs",
"hdrs_check",
"implementation_deps",
"include_prefix",
"strip_include_prefix",
"local_defines",
]
_CC_LIBRARY_ATTRS = {
"copts": [],
"defines": [],
"deps": [],
"hdrs": [],
"includes": [],
"srcs": [],
"textual_hdrs": [],
}
_CC_LIBRARY_REQUIRED_ATTRS = {
}
_IGNORE_OBJC_LIBRARY_ATTRS = [
"data",
"alwayslink",
"applicable_licenses",
"aspect_hints",
"compatible_with",
"enable_modules",
"exec_compatible_with",
"exec_properties",
"expect_failure",
"features",
"generator_function",
"generator_location",
"generator_name",
"deprecation",
"module_name",
"name",
"stamp",
"tags",
"target_compatible_with",
"testonly",
"to_json",
"to_proto",
"toolchains",
"transitive_configs",
"visibility",
]
_IGNORE_OBJC_LIBRARY_EMPTY_ATTRS = [
"implementation_deps",
"linkopts",
"module_map",
"non_arc_srcs",
"pch",
"restricted_to",
"textual_hdrs",
"sdk_includes",
]
_OBJC_LIBRARY_ATTRS = {
"copts": [],
"defines": [],
"deps": [],
"hdrs": [],
"srcs": [],
"sdk_dylibs": [],
"sdk_frameworks": [],
"weak_sdk_frameworks": [],
"includes": [],
}
_OBJC_LIBRARY_REQUIRED_ATTRS = [
"module_name",
]
_IGNORE_SWIFT_LIBRARY_ATTRS = [
"data",
"always_include_developer_search_paths",
"alwayslink",
"applicable_licenses",
"aspect_hints",
"compatible_with",
"deprecation",
"exec_compatible_with",
"exec_properties",
"expect_failure",
"features",
"generated_header_name",
"generates_header",
"generator_function",
"generator_location",
"generator_name",
"linkstatic",
"module_name",
"name",
"package_name",
"restricted_to",
"tags",
"target_compatible_with",
"testonly",
"to_json",
"to_proto",
"toolchains",
"transitive_configs",
"visibility",
]
_IGNORE_SWIFT_LIBRARY_EMPTY_ATTRS = [
"plugins",
"private_deps",
"swiftc_inputs",
]
_SWIFT_LIBRARY_ATTRS = {
"copts": [],
"defines": [],
"deps": [],
"linkopts": [],
"srcs": [],
}
_SWIFT_LIBRARY_REQUIRED_ATTRS = [
"module_name",
]
_LIBRARY_CONFIGS = {
"cc_library": {
"ignore_attrs": _IGNORE_CC_LIBRARY_ATTRS,
"ignore_empty_attrs": _IGNORE_CC_LIBRARY_EMPTY_ATTRS,
"handled_attrs": _CC_LIBRARY_ATTRS,
"required_attrs": _CC_LIBRARY_REQUIRED_ATTRS,
},
"objc_library": {
"ignore_attrs": _IGNORE_OBJC_LIBRARY_ATTRS,
"ignore_empty_attrs": _IGNORE_OBJC_LIBRARY_EMPTY_ATTRS,
"handled_attrs": _OBJC_LIBRARY_ATTRS,
"required_attrs": _OBJC_LIBRARY_REQUIRED_ATTRS,
},
"swift_library": {
"ignore_attrs": _IGNORE_SWIFT_LIBRARY_ATTRS,
"ignore_empty_attrs": _IGNORE_SWIFT_LIBRARY_EMPTY_ATTRS,
"handled_attrs": _SWIFT_LIBRARY_ATTRS,
"required_attrs": _SWIFT_LIBRARY_REQUIRED_ATTRS,
},
}
def get_rule_atts(rule):
if rule.kind in _LIBRARY_CONFIGS:
config = _LIBRARY_CONFIGS[rule.kind]
ignore_attrs = config["ignore_attrs"]
ignore_empty_attrs = config["ignore_empty_attrs"]
handled_attrs = config["handled_attrs"]
required_attrs = config["required_attrs"]
for attr_name in dir(rule.attr):
if attr_name.startswith("_"):
continue
if attr_name in ignore_attrs:
continue
if attr_name in ignore_empty_attrs:
attr_value = getattr(rule.attr, attr_name)
if attr_value == [] or attr_value == None or attr_value == "":
continue
else:
fail("Attribute {} is not empty: {}".format(attr_name, attr_value))
if attr_name in handled_attrs:
continue
fail("Unknown attribute: {}".format(attr_name))
result = dict()
result["type"] = rule.kind
for attr_name in handled_attrs:
if hasattr(rule.attr, attr_name):
result[attr_name] = getattr(rule.attr, attr_name)
else:
result[attr_name] = handled_attrs[attr_name] # Use default value
for attr_name in required_attrs:
if not hasattr(rule.attr, attr_name):
if rule.kind == "objc_library" and attr_name == "module_name":
result[attr_name] = getattr(rule.attr, "name")
else:
fail("Required attribute {} is missing".format(attr_name))
else:
result[attr_name] = getattr(rule.attr, attr_name)
result["name"] = getattr(rule.attr, "name")
return result
elif rule.kind == "ios_application":
result = dict()
result["type"] = "ios_application"
return result
elif rule.kind == "generate_spm":
result = dict()
result["type"] = "root"
return result
elif rule.kind == "apple_static_xcframework_import":
result = dict()
result["type"] = "apple_static_xcframework_import"
return result
else:
fail("Unknown rule kind: {}".format(rule.kind))
def _collect_spm_modules_impl(target, ctx):
# Skip targets without DefaultInfo
if not DefaultInfo in target:
return []
# Get module name
module_name = ctx.label.name
if hasattr(ctx.rule.attr, "module_name"):
module_name = ctx.rule.attr.module_name or ctx.label.name
# Collect all modules and transitive sources from dependencies first
all_modules = {}
dep_transitive_sources_list = []
if hasattr(ctx.rule.attr, "deps"):
for dep in ctx.rule.attr.deps:
if SPMModulesInfo in dep:
# Merge the modules dictionaries
for label, info in dep[SPMModulesInfo].modules.items():
all_modules[label] = info
# Add transitive sources depset from dependency to the list
dep_transitive_sources_list.append(dep[SPMModulesInfo].transitive_sources)
# Merge all transitive sources from dependencies
transitive_sources_from_deps = depset(transitive = dep_transitive_sources_list)
# Keep this for debugging later
# if result_attrs["type"] == "swift_library":
# print("Processing rule {}".format(ctx.label.name))
# print("ctx.rule.kind = {}".format(ctx.rule.kind))
# for attr_name in dir(ctx.rule.attr):
# print(" attr1: {}".format(attr_name))
result_attrs = get_rule_atts(ctx.rule)
sources = []
current_target_src_files = []
if "srcs" in result_attrs:
for src_target in result_attrs["srcs"]:
src_files = src_target.files.to_list()
for f in src_files:
if f.extension in ["swift", "cc", "cpp", "h", "m", "mm", "s", "S"]:
current_target_src_files.append(f)
for src_file in src_files:
sources.append(src_file.path)
current_target_sources = depset(current_target_src_files)
headers = []
current_target_hdr_files = []
if "hdrs" in result_attrs:
for hdr_target in result_attrs["hdrs"]:
hdr_files = hdr_target.files.to_list()
for f in hdr_files:
current_target_hdr_files.append(f)
for hdr_file in hdr_files:
headers.append(hdr_file.path)
current_target_headers = depset(current_target_hdr_files)
module_type = result_attrs["type"]
if module_type == "root":
pass
elif module_type == "apple_static_xcframework_import":
pass
elif module_type == "objc_library" or module_type == "swift_library" or module_type == "cc_library":
# Collect dependency labels
dep_names = []
if "deps" in result_attrs:
for dep in result_attrs["deps"]:
if hasattr(dep, "label"):
dep_label = str(dep.label)
dep_name = dep_label.split(":")[-1]
dep_names.append(dep_name)
else:
fail("Missing dependency label")
if module_type == "objc_library" or module_type == "swift_library":
if result_attrs["module_name"] != result_attrs["name"]:
fail("Module name mismatch: {} != {}".format(result_attrs["module_name"], result_attrs["name"]))
# Extract the path from the label
# Example: @//path/ModuleName:ModuleSubname -> path/ModuleName
if not str(ctx.label).startswith("@//"):
fail("Invalid label: {}".format(ctx.label))
module_path = str(ctx.label).split(":")[0].split("@//")[1]
if module_type == "objc_library":
module_info = {
"name": result_attrs["name"],
"type": module_type,
"path": module_path,
"defines": result_attrs["defines"],
"deps": dep_names,
"sources": sorted(sources + headers),
"module_name": module_name,
"copts": result_attrs["copts"],
"sdk_frameworks": result_attrs["sdk_frameworks"],
"sdk_dylibs": result_attrs["sdk_dylibs"],
"weak_sdk_frameworks": result_attrs["weak_sdk_frameworks"],
"includes": result_attrs["includes"],
}
elif module_type == "cc_library":
module_info = {
"name": result_attrs["name"],
"type": module_type,
"path": module_path,
"defines": result_attrs["defines"],
"deps": dep_names,
"sources": sorted(sources + headers),
"module_name": module_name,
"copts": result_attrs["copts"],
"includes": result_attrs["includes"],
}
elif module_type == "swift_library":
module_info = {
"name": result_attrs["name"],
"type": module_type,
"path": module_path,
"deps": dep_names,
"sources": sorted(sources),
"module_name": module_name,
"copts": result_attrs["copts"],
}
else:
fail("Unknown module type: {}".format(module_type))
if result_attrs["name"] in all_modules:
fail("Duplicate module name: {}".format(result_attrs["name"]))
all_modules[result_attrs["name"]] = module_info
elif result_attrs["type"] == "ios_application":
pass
else:
fail("Unknown rule type: {}".format(ctx.rule.kind))
# Add current target's sources and headers to the transitive set
final_transitive_sources = depset(transitive = [
transitive_sources_from_deps,
current_target_sources,
current_target_headers,
])
# Return both the SPM output files and the provider with modules data and sources
return [
SPMModulesInfo(
modules = all_modules,
transitive_sources = final_transitive_sources,
),
]
spm_modules_aspect = aspect(
implementation = _collect_spm_modules_impl,
attr_aspects = ["deps"],
)
def _generate_spm_impl(ctx):
outputs = []
dep_transitive_sources_list = []
if len(ctx.attr.deps) != 1:
fail("generate_spm must have exactly one dependency")
if SPMModulesInfo not in ctx.attr.deps[0]:
fail("generate_spm must have a dependency with SPMModulesInfo provider")
spm_info = ctx.attr.deps[0][SPMModulesInfo]
modules = spm_info.modules
# Declare and write the modules JSON file
modules_json_out = ctx.actions.declare_file("%s_modules.json" % ctx.label.name)
ctx.actions.write(
output = modules_json_out,
content = json.encode_indent(modules, indent = " "), # Use encode_indent for readability
)
outputs.append(modules_json_out)
for dep in ctx.attr.deps:
if SPMModulesInfo in dep:
# Add transitive sources depset from dependency
dep_transitive_sources_list.append(dep[SPMModulesInfo].transitive_sources)
# Merge all transitive sources from dependencies
transitive_sources_from_deps = depset(transitive = dep_transitive_sources_list)
# Return DefaultInfo containing only the output files in the 'files' field,
# but include the transitive sources in 'runfiles' to enforce the dependency.
return [DefaultInfo(
files = depset(outputs),
runfiles = ctx.runfiles(transitive_files = transitive_sources_from_deps),
)]
generate_spm = rule(
implementation = _generate_spm_impl,
attrs = {
'deps' : attr.label_list(aspects = [spm_modules_aspect]),
},
)

View File

@ -0,0 +1,193 @@
#! /usr/bin/env python3
import sys
import os
import sys
import json
import shutil
# Read the modules JSON file
modules_json_path = "bazel-bin/Telegram/spm_build_root_modules.json"
with open(modules_json_path, 'r') as f:
modules = json.load(f)
# Clean spm-files
spm_files_dir = "spm-files"
if os.path.exists(spm_files_dir):
shutil.rmtree(spm_files_dir)
if not os.path.exists(spm_files_dir):
os.makedirs(spm_files_dir)
combined_lines = []
combined_lines.append("// swift-tools-version: 6.0")
combined_lines.append("// The swift-tools-version declares the minimum version of Swift required to build this package.")
combined_lines.append("")
combined_lines.append("import PackageDescription")
combined_lines.append("")
combined_lines.append("let package = Package(")
combined_lines.append(" name: \"Telegram\",")
combined_lines.append(" platforms: [")
combined_lines.append(" .iOS(.v13)")
combined_lines.append(" ],")
combined_lines.append(" products: [")
for name, module in sorted(modules.items()):
if module["type"] == "objc_library" or module["type"] == "swift_library" or module["type"] == "cc_library":
combined_lines.append(" .library(name: \"%s\", targets: [\"%s\"])," % (module["name"], module["name"]))
combined_lines.append(" ],")
combined_lines.append(" targets: [")
for name, module in sorted(modules.items()):
module_type = module["type"]
if module_type == "objc_library" or module_type == "cc_library" or module_type == "swift_library":
combined_lines.append(" .target(")
combined_lines.append(" name: \"%s\"," % name)
linked_directory = None
has_non_linked_sources = False
for source in module["sources"]:
if source.startswith("bazel-out/"):
linked_directory = "spm-files/" + name
else:
has_non_linked_sources = True
if linked_directory and has_non_linked_sources:
print("Module {} has both regular and generated sources".format(name))
sys.exit(1)
if linked_directory:
os.makedirs(linked_directory)
combined_lines.append(" dependencies: [")
for dep in module["deps"]:
combined_lines.append(" .target(name: \"%s\")," % dep)
combined_lines.append(" ],")
if linked_directory:
combined_lines.append(" path: \"%s\"," % linked_directory)
else:
combined_lines.append(" path: \"%s\"," % module["path"])
combined_lines.append(" exclude: [")
exclude_files_and_dirs = []
if not linked_directory:
for root, dirs, files in os.walk(module["path"]):
rel_path = os.path.relpath(root, module["path"])
if rel_path == ".":
rel_path = ""
else:
rel_path += "/"
# Add directories that should be excluded
for d in dirs:
dir_path = os.path.join(rel_path, d)
if any(component.startswith('.') for component in dir_path.split('/')):
continue
# Check if any source file is under this directory
has_source = False
for source in module["sources"]:
rel_source = source[len(module["path"]) + 1:]
if rel_source.startswith(dir_path + "/"):
has_source = True
break
if not has_source:
exclude_files_and_dirs.append(dir_path)
# Add files that should be excluded
for f in files:
file_path = os.path.join(rel_path, f)
if any(component.startswith('.') for component in file_path.split('/')):
continue
if file_path not in [source[len(module["path"]) + 1:] for source in module["sources"]]:
exclude_files_and_dirs.append(file_path)
for item in exclude_files_and_dirs:
combined_lines.append(" \"%s\"," % item)
combined_lines.append(" ],")
combined_lines.append(" sources: [")
for source in module["sources"]:
if source.endswith(('.h', '.hpp')):
continue
linked_source_file_names = []
if not source.startswith(module["path"]):
if source.startswith("bazel-out/"):
if not linked_directory:
print("Source {} is a generated file, but module {} has no linked directory".format(source, name))
sys.exit(1)
if module["path"] in source:
source_file_name = source[source.index(module["path"]) + len(module["path"]) + 1:]
else:
print("Source {} is not inside module path {}".format(source, module["path"]))
sys.exit(1)
if source_file_name in linked_source_file_names:
print("Source {} is a duplicate".format(source))
sys.exit(1)
linked_source_file_names.append(source_file_name)
# Create any parent directories needed for the source file
symlink_location = os.path.join(linked_directory, source_file_name)
source_dir = os.path.dirname(symlink_location)
if not os.path.exists(source_dir):
os.makedirs(source_dir)
# Calculate the relative path from the symlink location back to the workspace root
num_parent_dirs = 2 + source_file_name.count(os.path.sep)
relative_prefix = "".join(["../"] * num_parent_dirs)
symlink_target = relative_prefix + source
os.symlink(symlink_target, symlink_location)
relative_source = source_file_name
combined_lines.append(" \"%s\"," % relative_source)
else:
print("Source {} is not inside module path {}".format(source, module["path"]))
sys.exit(1)
else:
relative_source = source[len(module["path"]) + 1:]
combined_lines.append(" \"%s\"," % relative_source)
combined_lines.append(" ],")
if module_type == "objc_library" or module_type == "cc_library":
if len(module["includes"]) == 0:
combined_lines.append(" publicHeadersPath: \"\",")
elif len(module["includes"]) == 1:
combined_lines.append(" publicHeadersPath: \"%s\"," % module["includes"][0])
else:
print("Multiple includes are not supported yet: {}".format(module["includes"]))
sys.exit(1)
combined_lines.append(" cSettings: [")
combined_lines.append(" .unsafeFlags([")
for flag in module["copts"]:
# Escape C-string entities in flag
escaped_flag = flag.replace('\\', '\\\\').replace('"', '\\"')
combined_lines.append(" \"%s\"," % escaped_flag)
combined_lines.append(" ])")
combined_lines.append(" ],")
combined_lines.append(" linkerSettings: [")
if module_type == "objc_library":
for framework in module["sdk_frameworks"]:
combined_lines.append(" .linkedFramework(\"%s\")," % framework)
for dylib in module["sdk_dylibs"]:
combined_lines.append(" .linkedLibrary(\"%s\")," % dylib)
combined_lines.append(" ]")
elif module_type == "swift_library":
combined_lines.append(" swiftSettings: [")
combined_lines.append(" .swiftLanguageMode(.v5),")
combined_lines.append(" .unsafeFlags([")
for flag in module["copts"]:
combined_lines.append(" \"%s\"," % flag)
combined_lines.append(" ])")
combined_lines.append(" ]")
combined_lines.append(" ),")
elif module["type"] == "root":
pass
else:
print("Unknown module type: {}".format(module["type"]))
sys.exit(1)
combined_lines.append(" ]")
combined_lines.append(")")
combined_lines.append("")
with open("Package.swift", "w") as f:
f.write("\n".join(combined_lines))