402 lines
12 KiB
Bash
Executable File
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
|