Files
ggbond268--mactools/scripts/plugins/build-local-plugins.sh
T
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

402 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
build-local-plugins.sh --source-dir Plugins --output-dir build/LocalPlugins [--plugin Demo]...
build-local-plugins.sh --source-dir Plugins --output-dir build/PluginRelease/Build --configuration Release --skip-catalog
Conventions:
- A plugin can be an existing .mactoolsplugin directory.
- Or a plugin source directory can contain plugin.json and the declared bundle.
- If the declared bundle is missing and the directory has project.yml or *.xcodeproj,
the script tries to build it with xcodebuild first.
- --plugin accepts a plugin directory name or plugin ID. It can be repeated or
passed as a comma-separated list.
- Set --sign-identity or PLUGIN_CODE_SIGN_IDENTITY to sign packaged bundles.
- Set --unsigned-build when a later packaging step will sign the bundle.
- Set --xcodebuild or XCODEBUILD to override the xcodebuild executable.
Generated output:
build/LocalPlugins/
Packages/*.mactoolsplugin
catalog.dev.json unless --skip-catalog is set
USAGE
}
SOURCE_DIR=""
OUTPUT_DIR=""
PLUGIN_FILTERS=()
CONFIGURATION="${CONFIGURATION:-Debug}"
DESTINATION="${DESTINATION:-}"
XCODEBUILD_COMMAND="${XCODEBUILD:-}"
SIGN_IDENTITY="${PLUGIN_CODE_SIGN_IDENTITY:-}"
SKIP_CATALOG=0
UNSIGNED_BUILD=0
while [[ $# -gt 0 ]]; do
case "$1" in
--source-dir)
SOURCE_DIR="${2:-}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:-}"
shift 2
;;
--plugin)
IFS=',' read -r -a raw_plugin_filters <<< "${2:-}"
for raw_plugin_filter in "${raw_plugin_filters[@]}"; do
raw_plugin_filter="${raw_plugin_filter#"${raw_plugin_filter%%[![:space:]]*}"}"
raw_plugin_filter="${raw_plugin_filter%"${raw_plugin_filter##*[![:space:]]}"}"
[[ -n "$raw_plugin_filter" ]] && PLUGIN_FILTERS+=("$raw_plugin_filter")
done
shift 2
;;
--configuration)
CONFIGURATION="${2:-}"
shift 2
;;
--destination)
DESTINATION="${2:-}"
shift 2
;;
--xcodebuild)
XCODEBUILD_COMMAND="${2:-}"
shift 2
;;
--sign-identity)
SIGN_IDENTITY="${2:-}"
shift 2
;;
--skip-catalog)
SKIP_CATALOG=1
shift
;;
--unsigned-build)
UNSIGNED_BUILD=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$SOURCE_DIR" || -z "$OUTPUT_DIR" ]]; then
usage >&2
exit 1
fi
SOURCE_DIR="$(cd "$SOURCE_DIR" 2>/dev/null && pwd || true)"
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Plugin source directory not found. Expected: $SOURCE_DIR" >&2
exit 1
fi
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
if [[ -z "$XCODEBUILD_COMMAND" ]]; then
XCODEBUILD_COMMAND="$REPO_ROOT/scripts/xcodebuild-filtered.sh"
fi
OUTPUT_DIR="$(mkdir -p "$OUTPUT_DIR" && cd "$OUTPUT_DIR" && pwd)"
PACKAGES_DIR="$OUTPUT_DIR/Packages"
DERIVED_DATA_DIR="$OUTPUT_DIR/DerivedData"
CATALOG_PATH="$OUTPUT_DIR/catalog.dev.json"
rm -rf "$PACKAGES_DIR"
mkdir -p "$PACKAGES_DIR" "$DERIVED_DATA_DIR"
json_value() {
local file="$1"
local expression="$2"
python3 - "$file" "$expression" <<'PY'
import json
import sys
path, expression = sys.argv[1:]
data = json.load(open(path))
value = data
for part in expression.split("."):
if not part:
continue
if isinstance(value, dict):
value = value.get(part)
else:
value = None
break
if value is None:
print("")
else:
print(value)
PY
}
discover_candidates() {
if [[ -d "$SOURCE_DIR" && "$SOURCE_DIR" == *.mactoolsplugin ]]; then
printf '%s\n' "$SOURCE_DIR"
return
fi
if [[ -f "$SOURCE_DIR/plugin.json" ]]; then
printf '%s\n' "$SOURCE_DIR"
return
fi
find "$SOURCE_DIR" -maxdepth 3 \( -name plugin.json -o -name '*.mactoolsplugin' \) -print \
| while IFS= read -r path; do
if [[ "$path" == *.mactoolsplugin ]]; then
printf '%s\n' "$path"
else
dirname "$path"
fi
done \
| sort -u
}
matches_filter() {
local candidate="$1"
if [[ ${#PLUGIN_FILTERS[@]} -eq 0 ]]; then
return 0
fi
local basename
basename="$(basename "$candidate")"
basename="${basename%.mactoolsplugin}"
local id=""
if [[ -f "$candidate/plugin.json" ]]; then
id="$(json_value "$candidate/plugin.json" "id")"
fi
local filter
for filter in "${PLUGIN_FILTERS[@]}"; do
[[ "$basename" == "$filter" ]] && return 0
[[ -n "$id" && "$id" == "$filter" ]] && return 0
done
return 1
}
project_file_for() {
local root="$1"
local manifest="$2"
local output_var="$3"
local configured_project
configured_project="$(json_value "$manifest" "build.project")"
[[ -z "$configured_project" ]] && configured_project="$(json_value "$manifest" "buildProject")"
if [[ -n "$configured_project" ]]; then
local project_path
if [[ "$configured_project" = /* ]]; then
project_path="$configured_project"
else
project_path="$(cd "$root" && cd "$(dirname "$configured_project")" && printf '%s/%s\n' "$(pwd)" "$(basename "$configured_project")")"
fi
printf -v "$output_var" '%s' "$project_path"
return
fi
if [[ -f "$root/project.yml" ]]; then
if ! (cd "$root" && xcodegen generate >/dev/null); then
echo "Failed to generate Xcode project for $root." >&2
return 1
fi
fi
local project_path
project_path="$(find "$root" -maxdepth 1 -name '*.xcodeproj' -print | sort | head -1)"
printf -v "$output_var" '%s' "$project_path"
}
scheme_for() {
local root="$1"
local project="$2"
local manifest="$3"
local output_var="$4"
local candidate_scheme
candidate_scheme="$(json_value "$manifest" "build.scheme")"
[[ -n "$candidate_scheme" ]] && { printf -v "$output_var" '%s' "$candidate_scheme"; return; }
candidate_scheme="$(json_value "$manifest" "buildScheme")"
[[ -n "$candidate_scheme" ]] && { printf -v "$output_var" '%s' "$candidate_scheme"; return; }
local root_name
root_name="$(basename "$root")"
if "$XCODEBUILD_COMMAND" -list -project "$project" 2>/dev/null | grep -q "^[[:space:]]*$root_name$"; then
printf -v "$output_var" '%s' "$root_name"
return
fi
candidate_scheme="$("$XCODEBUILD_COMMAND" -list -json -project "$project" 2>/dev/null \
| python3 -c 'import json,sys; data=json.load(sys.stdin); print((data.get("project") or {}).get("schemes", [""])[0])')"
printf -v "$output_var" '%s' "$candidate_scheme"
}
build_bundle_if_needed() {
local root="$1"
local manifest="$2"
local bundle_relative_path="$3"
local bundle_name="$4"
local output_var="$5"
if [[ -d "$root/$bundle_relative_path" ]]; then
printf -v "$output_var" '%s' "$root/$bundle_relative_path"
return
fi
local project
project_file_for "$root" "$manifest" project
if [[ -z "$project" ]]; then
echo "Bundle not found and no Xcode project exists: $root/$bundle_relative_path" >&2
return 1
fi
local scheme
scheme_for "$root" "$project" "$manifest" scheme
if [[ -z "$scheme" ]]; then
echo "Unable to infer xcodebuild scheme for $root" >&2
return 1
fi
local derived_data
derived_data="$DERIVED_DATA_DIR/$(basename "$root")"
rm -rf "$derived_data"
local build_args=(
-project "$project"
-scheme "$scheme"
-configuration "$CONFIGURATION"
-derivedDataPath "$derived_data"
)
if [[ -n "$DESTINATION" ]]; then
build_args+=(-destination "$DESTINATION")
fi
if [[ "$UNSIGNED_BUILD" == "1" ]]; then
build_args+=(
CODE_SIGNING_ALLOWED=NO
CODE_SIGNING_REQUIRED=NO
CODE_SIGN_IDENTITY=
)
fi
build_args+=(build -quiet)
if ! "$XCODEBUILD_COMMAND" "${build_args[@]}"; then
echo "Failed to build plugin bundle '$scheme' for $root." >&2
return 1
fi
local built_bundle
built_bundle="$(find "$derived_data/Build/Products" -name "$bundle_name" -type d -print | sort | head -1)"
if [[ -z "$built_bundle" ]]; then
echo "Built bundle not found: $bundle_name" >&2
return 1
fi
printf -v "$output_var" '%s' "$built_bundle"
}
package_source_dir() {
local root="$1"
local output_var="$2"
local manifest="$root/plugin.json"
if [[ ! -f "$manifest" ]]; then
echo "Missing plugin.json: $root" >&2
return 1
fi
local plugin_id
local bundle_relative_path
local bundle_name
plugin_id="$(json_value "$manifest" "id")"
bundle_relative_path="$(json_value "$manifest" "bundleRelativePath")"
bundle_name="$(basename "$bundle_relative_path")"
if [[ -z "$plugin_id" || -z "$bundle_relative_path" ]]; then
echo "plugin.json must include id and bundleRelativePath: $manifest" >&2
return 1
fi
local bundle_path
build_bundle_if_needed "$root" "$manifest" "$bundle_relative_path" "$bundle_name" bundle_path
local package_path="$PACKAGES_DIR/$plugin_id.mactoolsplugin"
rm -rf "$package_path"
mkdir -p "$package_path/$(dirname "$bundle_relative_path")"
ditto "$manifest" "$package_path/plugin.json"
ditto "$bundle_path" "$package_path/$bundle_relative_path"
if [[ -n "$SIGN_IDENTITY" ]]; then
"$REPO_ROOT/scripts/plugins/sign-plugin-package.sh" \
--package "$package_path" \
--identity "$SIGN_IDENTITY"
fi
[[ -d "$package_path" ]] || { echo "Failed to create plugin package: $package_path" >&2; return 1; }
printf -v "$output_var" '%s' "$package_path"
}
copy_package_dir() {
local package="$1"
local output_var="$2"
local package_name
package_name="$(basename "$package")"
local destination="$PACKAGES_DIR/$package_name"
rm -rf "$destination"
"$REPO_ROOT/scripts/plugins/build-plugin-package.sh" \
--source "$package" \
--output-dir "$PACKAGES_DIR" >/dev/null
printf -v "$output_var" '%s' "$destination"
}
packages=()
while IFS= read -r candidate; do
[[ -n "$candidate" ]] || continue
matches_filter "$candidate" || continue
if [[ "$candidate" == *.mactoolsplugin ]]; then
built_package_path=""
copy_package_dir "$candidate" built_package_path
packages+=("$built_package_path")
else
built_package_path=""
package_source_dir "$candidate" built_package_path
packages+=("$built_package_path")
fi
done < <(discover_candidates)
if [[ ${#packages[@]} -eq 0 ]]; then
if [[ ${#PLUGIN_FILTERS[@]} -gt 0 ]]; then
echo "No plugin matched requested filters in $SOURCE_DIR: ${PLUGIN_FILTERS[*]}" >&2
else
echo "No plugins found in $SOURCE_DIR." >&2
fi
exit 1
fi
if [[ "$SKIP_CATALOG" != "1" ]]; then
catalog_args=()
for package in "${packages[@]}"; do
catalog_args+=(--package "$package")
done
"$REPO_ROOT/scripts/plugins/generate-plugin-catalog.sh" \
--mode debug \
--output "$CATALOG_PATH" \
"${catalog_args[@]}"
fi
echo "Built ${#packages[@]} plugin package(s)."
if [[ "$SKIP_CATALOG" != "1" ]]; then
echo "Catalog: $CATALOG_PATH"
fi