414 lines
13 KiB
Ruby
Executable File
414 lines
13 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require "json"
|
|
require "optparse"
|
|
require "fileutils"
|
|
require "pathname"
|
|
require "shellwords"
|
|
require "yaml"
|
|
|
|
options = {
|
|
source_dir: "Plugins",
|
|
output: "Configs/GeneratedPlugins.yml"
|
|
}
|
|
|
|
OptionParser.new do |opts|
|
|
opts.banner = "Usage: generate-plugin-project-config.rb [--source-dir Plugins] [--output Configs/GeneratedPlugins.yml]"
|
|
opts.on("--source-dir PATH", "Plugin source directory") { |value| options[:source_dir] = value }
|
|
opts.on("--output PATH", "Generated XcodeGen spec output") { |value| options[:output] = value }
|
|
end.parse!
|
|
|
|
repo_root = File.expand_path("../..", __dir__)
|
|
source_dir = File.expand_path(options[:source_dir], repo_root)
|
|
output_path = File.expand_path(options[:output], repo_root)
|
|
output_dir = File.dirname(output_path)
|
|
|
|
unless Dir.exist?(source_dir)
|
|
warn "Plugin source directory not found: #{source_dir}"
|
|
exit 1
|
|
end
|
|
|
|
def read_yaml(path)
|
|
return {} unless File.file?(path)
|
|
|
|
YAML.safe_load(File.read(path), permitted_classes: [Symbol], aliases: true) || {}
|
|
rescue Psych::SyntaxError => e
|
|
warn "Invalid YAML in #{path}: #{e.message}"
|
|
exit 1
|
|
end
|
|
|
|
def camelize_id(value)
|
|
value
|
|
.split(/[^A-Za-z0-9]+/)
|
|
.reject(&:empty?)
|
|
.map { |part| part[0].upcase + part[1..] }
|
|
.join
|
|
end
|
|
|
|
def deep_merge(left, right)
|
|
left.merge(right) do |_key, old_value, new_value|
|
|
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
|
deep_merge(old_value, new_value)
|
|
else
|
|
new_value
|
|
end
|
|
end
|
|
end
|
|
|
|
def relative_to_output_dir(path, output_dir)
|
|
Pathname(File.expand_path(path)).relative_path_from(Pathname(output_dir)).to_s
|
|
end
|
|
|
|
def normalize_fragment_path(plugin_relative_dir, item, repo_root, output_dir)
|
|
case item
|
|
when String
|
|
{ "path" => relative_to_output_dir(File.join(repo_root, plugin_relative_dir, item), output_dir) }
|
|
when Hash
|
|
normalized = item.transform_keys(&:to_s)
|
|
path = normalized["path"]
|
|
if path && !path.start_with?("/", "$(")
|
|
absolute_path = if path.start_with?("#{plugin_relative_dir}/")
|
|
File.join(repo_root, path)
|
|
else
|
|
File.join(repo_root, plugin_relative_dir, path)
|
|
end
|
|
normalized["path"] = relative_to_output_dir(absolute_path, output_dir)
|
|
end
|
|
normalized
|
|
else
|
|
item
|
|
end
|
|
end
|
|
|
|
def split_setting_words(value)
|
|
return [] if value.nil? || value.to_s.strip.empty?
|
|
|
|
Shellwords.split(value.to_s)
|
|
rescue ArgumentError
|
|
value.to_s.split
|
|
end
|
|
|
|
def collect_words(target, value)
|
|
split_setting_words(value).each do |word|
|
|
target << word unless target.include?(word)
|
|
end
|
|
end
|
|
|
|
def collect_ldflags(target, value)
|
|
words = split_setting_words(value)
|
|
index = 0
|
|
while index < words.length
|
|
word = words[index]
|
|
if ["-framework", "-weak_framework", "-F"].include?(word) && index + 1 < words.length
|
|
flag = "#{word} #{words[index + 1]}"
|
|
target << flag unless target.include?(flag)
|
|
index += 2
|
|
else
|
|
target << word unless target.include?(word)
|
|
index += 1
|
|
end
|
|
end
|
|
end
|
|
|
|
def yaml_scalar(value)
|
|
case value
|
|
when true
|
|
"true"
|
|
when false
|
|
"false"
|
|
when Numeric
|
|
value.to_s
|
|
else
|
|
string = value.to_s
|
|
if string.empty? || string.match?(/[:#\[\]\{\},&\*!\|>'"%@`]/) || string.match?(/\A(?:true|false|null|yes|no|on|off|\d)/i) || string.include?("$(")
|
|
string.dump
|
|
else
|
|
string
|
|
end
|
|
end
|
|
end
|
|
|
|
def write_yaml(io, value, indent = 0)
|
|
spaces = " " * indent
|
|
case value
|
|
when Hash
|
|
value.each do |key, child|
|
|
if child.is_a?(Hash)
|
|
io << "#{spaces}#{key}:\n"
|
|
write_yaml(io, child, indent + 2)
|
|
elsif child.is_a?(Array)
|
|
io << "#{spaces}#{key}:\n"
|
|
write_yaml(io, child, indent + 2)
|
|
else
|
|
io << "#{spaces}#{key}: #{yaml_scalar(child)}\n"
|
|
end
|
|
end
|
|
when Array
|
|
value.each do |child|
|
|
if child.is_a?(Hash)
|
|
first = true
|
|
child.each do |key, nested|
|
|
prefix = first ? "#{spaces}- " : "#{" " * (indent + 2)}"
|
|
if nested.is_a?(Hash) || nested.is_a?(Array)
|
|
io << "#{prefix}#{key}:\n"
|
|
write_yaml(io, nested, indent + 4)
|
|
else
|
|
io << "#{prefix}#{key}: #{yaml_scalar(nested)}\n"
|
|
end
|
|
first = false
|
|
end
|
|
else
|
|
io << "#{spaces}- #{yaml_scalar(child)}\n"
|
|
end
|
|
end
|
|
else
|
|
io << "#{spaces}#{yaml_scalar(value)}\n"
|
|
end
|
|
end
|
|
|
|
plugin_roots = Dir.children(source_dir)
|
|
.map { |entry| File.join(source_dir, entry) }
|
|
.select { |path| File.file?(File.join(path, "plugin.json")) }
|
|
.sort_by { |path| File.basename(path).downcase }
|
|
|
|
targets = {}
|
|
plugin_schemes = {}
|
|
plugin_bundle_targets = []
|
|
plugin_core_targets = []
|
|
test_include_paths = []
|
|
test_ldflags = []
|
|
|
|
plugin_roots.each do |plugin_root|
|
|
manifest_path = File.join(plugin_root, "plugin.json")
|
|
manifest = JSON.parse(File.read(manifest_path))
|
|
fragment = read_yaml(File.join(plugin_root, "project.yml")).transform_keys(&:to_s)
|
|
build = (manifest["build"] || {}).merge(fragment.fetch("build", {}))
|
|
|
|
plugin_id = manifest.fetch("id")
|
|
plugin_relative_dir = Pathname(File.expand_path(plugin_root)).relative_path_from(Pathname(repo_root)).to_s
|
|
scheme = build["scheme"] || "#{camelize_id(plugin_id)}Plugin"
|
|
module_name = build["moduleName"] || manifest["factoryClass"].to_s.split(".").first || scheme
|
|
bundle_relative_path = manifest.fetch("bundleRelativePath")
|
|
product_name = build["productName"] || File.basename(bundle_relative_path, ".bundle")
|
|
bundle_module_name = build["bundleModuleName"] || "#{module_name}Bundle"
|
|
core_target = build["coreTarget"] || "#{scheme}Core"
|
|
bundle_target = build["bundleTarget"] || scheme
|
|
|
|
common_settings = {
|
|
"GENERATE_INFOPLIST_FILE" => "YES",
|
|
"SWIFT_VERSION" => "6.0",
|
|
"DEVELOPMENT_LANGUAGE" => "zh-Hans",
|
|
"MACOSX_DEPLOYMENT_TARGET" => "14.0",
|
|
"CODE_SIGN_STYLE" => "Automatic",
|
|
"DEVELOPMENT_TEAM" => "$(DEVELOPMENT_TEAM)"
|
|
}
|
|
|
|
shared_settings = fragment.fetch("settings", {})
|
|
core_settings = deep_merge(
|
|
{
|
|
"base" => common_settings.merge(
|
|
"PRODUCT_MODULE_NAME" => module_name,
|
|
"PRODUCT_NAME" => core_target,
|
|
"PRODUCT_BUNDLE_IDENTIFIER" => "$(BUNDLE_IDENTIFIER_PREFIX).mactools.plugins.#{plugin_id}.core"
|
|
)
|
|
},
|
|
shared_settings
|
|
)
|
|
core_settings = deep_merge(core_settings, fragment.dig("core", "settings") || {})
|
|
|
|
bundle_settings = deep_merge(
|
|
{
|
|
"base" => common_settings.merge(
|
|
"PRODUCT_NAME" => product_name,
|
|
"PRODUCT_MODULE_NAME" => bundle_module_name,
|
|
"PRODUCT_BUNDLE_IDENTIFIER" => "$(BUNDLE_IDENTIFIER_PREFIX).mactools.plugins.#{plugin_id}"
|
|
)
|
|
},
|
|
shared_settings
|
|
)
|
|
bundle_settings = deep_merge(bundle_settings, fragment.dig("bundle", "settings") || {})
|
|
|
|
[core_settings, bundle_settings, fragment.dig("tests", "settings") || {}].each do |settings|
|
|
base = settings["base"] || {}
|
|
collect_words(test_include_paths, base["SWIFT_INCLUDE_PATHS"])
|
|
collect_ldflags(test_ldflags, base["OTHER_LDFLAGS"])
|
|
end
|
|
|
|
core_sources = [{ "path" => relative_to_output_dir(File.join(plugin_root, "Sources"), output_dir) }]
|
|
Array(fragment.dig("core", "sources")).each do |item|
|
|
core_sources << normalize_fragment_path(plugin_relative_dir, item, repo_root, output_dir)
|
|
end
|
|
|
|
bundle_sources = [{ "path" => relative_to_output_dir(File.join(plugin_root, "Bundle"), output_dir) }]
|
|
plugin_resources_dir = File.join(plugin_root, "Resources")
|
|
if Dir.exist?(plugin_resources_dir)
|
|
bundle_sources << { "path" => relative_to_output_dir(plugin_resources_dir, output_dir) }
|
|
end
|
|
Array(fragment.dig("bundle", "sources")).each do |item|
|
|
bundle_sources << normalize_fragment_path(plugin_relative_dir, item, repo_root, output_dir)
|
|
end
|
|
|
|
extra_targets = {}
|
|
extra_bundle_dependencies = []
|
|
extra_scheme_targets = []
|
|
generated_post_build_scripts = []
|
|
Array(fragment.fetch("targets", {})).each do |target_name, target_spec|
|
|
target_name = target_name.to_s
|
|
normalized_spec = target_spec.transform_keys(&:to_s)
|
|
normalized_sources = []
|
|
Array(normalized_spec["sources"]).each do |item|
|
|
normalized_sources << normalize_fragment_path(plugin_relative_dir, item, repo_root, output_dir)
|
|
end
|
|
|
|
normalized_settings = normalized_spec["settings"] || {}
|
|
base_settings = normalized_settings["base"] || {}
|
|
collect_words(test_include_paths, base_settings["SWIFT_INCLUDE_PATHS"])
|
|
collect_ldflags(test_ldflags, base_settings["OTHER_LDFLAGS"])
|
|
|
|
target_hash = normalized_spec.reject { |key, _| key == "bundleResourcePath" }
|
|
target_hash["sources"] = normalized_sources unless normalized_sources.empty?
|
|
target_dependencies = Array(normalized_spec["dependencies"])
|
|
target_hash["dependencies"] = target_dependencies unless target_dependencies.empty?
|
|
target_hash["settings"] = normalized_settings unless normalized_settings.empty?
|
|
extra_targets[target_name] = target_hash
|
|
|
|
resource_path = normalized_spec["bundleResourcePath"]
|
|
next unless resource_path
|
|
|
|
extra_bundle_dependencies << { "target" => target_name, "link" => false }
|
|
extra_scheme_targets << target_name
|
|
generated_post_build_scripts << {
|
|
"name" => "Copy #{target_name}",
|
|
"script" => [
|
|
"set -euo pipefail",
|
|
"resource_dir=\"$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/#{resource_path}\"",
|
|
"mkdir -p \"$resource_dir\"",
|
|
"ditto \"$BUILT_PRODUCTS_DIR/#{target_name}\" \"$resource_dir/#{target_name}\"",
|
|
"chmod 755 \"$resource_dir/#{target_name}\""
|
|
].join("\n"),
|
|
"inputFiles" => ["$(BUILT_PRODUCTS_DIR)/#{target_name}"],
|
|
"outputFiles" => ["$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/#{resource_path}/#{target_name}"]
|
|
}
|
|
end
|
|
|
|
targets.merge!(extra_targets)
|
|
|
|
targets[core_target] = {
|
|
"type" => "library.static",
|
|
"platform" => "macOS",
|
|
"deploymentTarget" => "14.0",
|
|
"configFiles" => {
|
|
"Debug" => "Debug.xcconfig",
|
|
"Release" => "Release.xcconfig"
|
|
},
|
|
"sources" => core_sources,
|
|
"dependencies" => [{ "target" => "MacToolsPluginKit" }] + Array(fragment.dig("core", "dependencies")),
|
|
"settings" => core_settings
|
|
}
|
|
|
|
bundle_post_build_scripts = Array(fragment.dig("bundle", "postBuildScripts")) + generated_post_build_scripts
|
|
|
|
bundle_target_hash = {
|
|
"type" => "bundle",
|
|
"platform" => "macOS",
|
|
"deploymentTarget" => "14.0",
|
|
"configFiles" => {
|
|
"Debug" => "Debug.xcconfig",
|
|
"Release" => "Release.xcconfig"
|
|
},
|
|
"sources" => bundle_sources,
|
|
"dependencies" => [
|
|
{ "target" => "MacToolsPluginKit" },
|
|
{ "target" => core_target, "link" => true }
|
|
] + extra_bundle_dependencies + Array(fragment.dig("bundle", "dependencies")),
|
|
"settings" => bundle_settings
|
|
}
|
|
bundle_target_hash["postBuildScripts"] = bundle_post_build_scripts unless bundle_post_build_scripts.empty?
|
|
targets[bundle_target] = bundle_target_hash
|
|
|
|
plugin_bundle_targets << bundle_target
|
|
plugin_core_targets << core_target
|
|
plugin_schemes[bundle_target] = {
|
|
"build" => {
|
|
"targets" => {
|
|
"MacToolsPluginKit" => "all",
|
|
core_target => "all",
|
|
bundle_target => "all"
|
|
}
|
|
.merge(extra_scheme_targets.to_h { |target| [target, "all"] })
|
|
},
|
|
"profile" => { "config" => "Release" },
|
|
"archive" => { "config" => "Release" }
|
|
}
|
|
end
|
|
|
|
test_settings = {
|
|
"base" => {
|
|
"PRODUCT_BUNDLE_IDENTIFIER" => "$(BUNDLE_IDENTIFIER_PREFIX).mactoolsTests",
|
|
"GENERATE_INFOPLIST_FILE" => "YES"
|
|
},
|
|
"configs" => {
|
|
"Debug" => {
|
|
"TEST_HOST" => "$(BUILT_PRODUCTS_DIR)/MacTools Dev.app/Contents/MacOS/MacTools Dev"
|
|
},
|
|
"Release" => {
|
|
"TEST_HOST" => "$(BUILT_PRODUCTS_DIR)/MacTools.app/Contents/MacOS/MacTools"
|
|
}
|
|
}
|
|
}
|
|
test_settings["base"]["SWIFT_INCLUDE_PATHS"] = test_include_paths.join(" ") unless test_include_paths.empty?
|
|
test_settings["base"]["OTHER_LDFLAGS"] = test_ldflags.join(" ") unless test_ldflags.empty?
|
|
|
|
targets["MacToolsTests"] = {
|
|
"type" => "bundle.unit-test",
|
|
"platform" => "macOS",
|
|
"deploymentTarget" => "14.0",
|
|
"sources" => [
|
|
{ "path" => relative_to_output_dir(File.join(repo_root, "Tests"), output_dir) },
|
|
{
|
|
"path" => relative_to_output_dir(File.join(repo_root, "Plugins"), output_dir),
|
|
"includes" => ["*/Tests/**"]
|
|
}
|
|
],
|
|
"dependencies" => [
|
|
{ "target" => "MacTools" },
|
|
{ "target" => "MacToolsPluginKit" }
|
|
] + plugin_core_targets.map { |target| { "target" => target } },
|
|
"settings" => test_settings
|
|
}
|
|
|
|
schemes = {
|
|
"MacTools" => {
|
|
"build" => {
|
|
"targets" => {
|
|
"MacToolsPluginKit" => "all"
|
|
}.merge(plugin_bundle_targets.to_h { |target| [target, "all"] })
|
|
.merge(
|
|
"MacTools" => "all",
|
|
"MacToolsTests" => ["test"]
|
|
)
|
|
},
|
|
"run" => { "config" => "Debug" },
|
|
"test" => {
|
|
"config" => "Debug",
|
|
"targets" => [
|
|
{
|
|
"name" => "MacToolsTests",
|
|
"parallelizable" => true
|
|
}
|
|
]
|
|
},
|
|
"profile" => { "config" => "Release" },
|
|
"archive" => { "config" => "Release" }
|
|
}
|
|
}.merge(plugin_schemes)
|
|
|
|
generated = +"# Generated by scripts/plugins/generate-plugin-project-config.rb. Do not edit.\n"
|
|
write_yaml(generated, "targets" => targets, "schemes" => schemes)
|
|
|
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
if !File.file?(output_path) || File.read(output_path) != generated
|
|
File.write(output_path, generated)
|
|
end
|