Extend macOS bundle script with release checks#779
Conversation
Add release-oriented subcommands to bundle-with-homebrew.sh: check, bundle, verify, and all. The new check step validates the Homebrew-based macOS bundling environment before packaging, while verify can inspect a generated DMG artifact. Add Makefile entry points for the release workflow. These targets delegate to the bundle script instead of implementing release logic in Makefile: - make release-check - make release - make release-test make release now runs the check step before bundling.
|
Please use the following format to refer to the associated issue in the future:
You used "Fixes number" in the PR comment and I changed it. See Pull Request Guidelines:
|
iefiru
left a comment
There was a problem hiding this comment.
I added inline comments around the release workflow. Feel free to share any questions or suggestions.
| .PHONY: release-check | ||
| release-check: | ||
| $(MODMESH_ROOT)/contrib/bundle/bundle-with-homebrew.sh check | ||
|
|
||
| .PHONY: release | ||
| release: | ||
| $(MODMESH_ROOT)/contrib/bundle/bundle-with-homebrew.sh all \ | ||
| --output "$(RELEASE_OUTPUT)" $(RELEASE_ARGS) | ||
|
|
||
| .PHONY: release-test | ||
| release-test: | ||
| $(MODMESH_ROOT)/contrib/bundle/bundle-with-homebrew.sh verify \ | ||
| "$(RELEASE_ARTIFACT)" | ||
|
|
There was a problem hiding this comment.
release-checkchecks the bundle dependencies.releasechecks the dependencies and builds the DMG bundle.release-testtests the DMG bundle.
There was a problem hiding this comment.
Discuss: I propose to use explicit names for macos: bundle-precheck (from release-check), bundle (from release), bundle-test (release-test).
| verify) | ||
| [[ $# -eq 1 ]] || { usage >&2; exit 2; } | ||
| VERIFY_ARTIFACT="$1" | ||
| shift | ||
| ;; |
There was a problem hiding this comment.
MUST provide DMG file for verify.
| cd "$BUNDLE_REPO_ROOT" | ||
|
|
||
| MIN_LOADS=${MIN_LOADS:-50} | ||
| HOST_PREFIX_RE='^(/opt/homebrew|/usr/local)' |
There was a problem hiding this comment.
Centralize host package path patterns for easier management and reuse.
| Subcommands: | ||
| check Check macOS bundle release dependencies. Does not build or install. | ||
| bundle Build/package pilot.app and pilot.dmg with Homebrew dependencies. | ||
| verify Verify a generated release DMG artifact. | ||
| all Run check, then bundle. |
There was a problem hiding this comment.
checkchecks the bundle dependencies.bundlechecks the dependencies and builds the DMG bundle.verifytests the DMG bundle.
There was a problem hiding this comment.
If all does not include verify, make it explicit like:
Run check and then bundle. (No verify.)
| python_module_check() { | ||
| local module="$1" hint="${2:-}" out | ||
| out=$(mktemp -t modmesh-bundle-pycheck) | ||
| if python3 - "$module" <<'PY' >"$out" 2>&1 | ||
| import importlib | ||
| import sys | ||
| module = sys.argv[1] | ||
| try: | ||
| mod = importlib.import_module(module) | ||
| except Exception as e: | ||
| print(f"{type(e).__name__}: {e}") | ||
| raise SystemExit(1) | ||
| else: | ||
| print(getattr(mod, "__file__", "built-in")) | ||
| PY | ||
| then | ||
| ok "python module $module: $(cat "$out")" | ||
| rm -f "$out" | ||
| else | ||
| cat "$out" >&2 || true | ||
| rm -f "$out" | ||
| fail "python module $module: missing${hint:+ ($hint)}" | ||
| fi | ||
| } |
There was a problem hiding this comment.
Import the given Python package to verify it is available.
| check_pybind11_cmake() { | ||
| local config_dir prefix | ||
| if command -v pybind11-config >/dev/null 2>&1; then | ||
| config_dir=$(pybind11-config --cmakedir 2>/dev/null || true) | ||
| if [[ -n "$config_dir" && -f "$config_dir/pybind11Config.cmake" ]]; then | ||
| ok "pybind11 CMake config: $config_dir/pybind11Config.cmake" | ||
| return 0 | ||
| fi | ||
| fi | ||
| prefix=$(brew --prefix pybind11 2>/dev/null || true) | ||
| if [[ -n "$prefix" && -f "$prefix/share/cmake/pybind11/pybind11Config.cmake" ]]; then | ||
| ok "pybind11 CMake config: $prefix/share/cmake/pybind11/pybind11Config.cmake" | ||
| else | ||
| fail "pybind11 CMake config: not found (brew install pybind11)" | ||
| fi | ||
| } |
There was a problem hiding this comment.
Ensure pybind11's CMake config is available before the build step.
| check_deps() { | ||
| local brew_exe brew_prefix python_path fw_path failed=0 | ||
| require_macos | ||
|
|
||
| if brew_exe=$(find_brew); then | ||
| ok "brew: $brew_exe" | ||
| eval "$("$brew_exe" shellenv)" | ||
| brew_prefix=$(brew --prefix) | ||
| ok "Homebrew prefix: $brew_prefix" | ||
| else | ||
| fail "brew: not found (install Homebrew)" || true | ||
| setup_hint | ||
| exit 1 | ||
| fi | ||
|
|
||
| for cmd in cmake make python3 macdeployqt qtpaths otool \ | ||
| install_name_tool codesign hdiutil rsync shasum; do | ||
| require_command "$cmd" || failed=1 | ||
| done | ||
|
|
||
| python_path=$(command -v python3 || true) | ||
| if [[ -n "$python_path" && "$python_path" == "$brew_prefix"/* ]]; then | ||
| ok "python3 is under Homebrew prefix" | ||
| else | ||
| fail "python3 is not from Homebrew prefix ($python_path; expected under $brew_prefix)" || failed=1 | ||
| fi | ||
|
|
||
| if fw_path=$(python_framework_check 2>&1); then | ||
| ok "Python framework dylib: $fw_path" | ||
| else | ||
| fail "$fw_path" || failed=1 | ||
| fi | ||
|
|
||
| check_pybind11_cmake || failed=1 | ||
| python_module_check numpy "brew install numpy" || failed=1 | ||
| python_module_check PySide6 "brew install pyside" || failed=1 | ||
| python_module_check shiboken6 "brew install pyside" || failed=1 | ||
| python_module_check shiboken6_generator "brew install pyside" || failed=1 | ||
| python_module_check matplotlib "brew install python-matplotlib" || failed=1 | ||
|
|
||
| if [[ $failed -ne 0 ]]; then | ||
| setup_hint | ||
| exit 1 | ||
| fi | ||
| } |
There was a problem hiding this comment.
Implement the check subcommand by running the macOS release preflight checks.
| verify_app_structure() { | ||
| local app="$1" bin | ||
| [[ -d "$app" ]] || fail "app not found: $app" | ||
| [[ -f "$app/Contents/Info.plist" ]] || fail "Info.plist not found" | ||
| bin="$app/Contents/MacOS/$(basename "$app" .app)" | ||
| [[ -x "$bin" ]] || fail "main binary not executable: $bin" | ||
| [[ -d "$app/Contents/Frameworks" ]] || fail "Contents/Frameworks not found" | ||
| [[ -d "$app/Contents/Resources" ]] || fail "Contents/Resources not found" | ||
| file "$bin" | ||
| } |
There was a problem hiding this comment.
Check that the DMG contains a complete macOS app bundle.
| smoke_launch_verify() { | ||
| local bin="$1" trace="$2" pid elapsed=0 loaded | ||
| : > "$trace" | ||
| env -i HOME="${HOME:-/}" USER="${USER:-nobody}" \ | ||
| PATH=/usr/bin:/bin TERM="${TERM:-xterm}" \ | ||
| DYLD_PRINT_LIBRARIES=1 \ | ||
| "$bin" >/dev/null 2>"$trace" & | ||
| pid=$! | ||
| while [[ $elapsed -lt 15 ]]; do | ||
| sleep 1 | ||
| elapsed=$((elapsed + 1)) | ||
| loaded=$(grep -c '^dyld\[' "$trace" 2>/dev/null || true) | ||
| [[ ${loaded:-0} -ge $MIN_LOADS ]] && break | ||
| done | ||
| kill "$pid" 2>/dev/null || true | ||
| wait "$pid" 2>/dev/null || true | ||
|
|
||
| loaded=$(grep -c '^dyld\[' "$trace" 2>/dev/null || true) | ||
| if [[ $loaded -lt $MIN_LOADS ]]; then | ||
| echo "ERROR: pilot loaded only $loaded libraries; did it crash early?" >&2 | ||
| sed -n '1,80p' "$trace" >&2 | ||
| return 1 | ||
| fi | ||
| if grep -E '^dyld\[' "$trace" | grep -E '/opt/homebrew|/usr/local' >/dev/null; then | ||
| echo "ERROR: smoke launch loaded libraries from host package prefixes:" >&2 | ||
| grep -E '^dyld\[' "$trace" | grep -E '/opt/homebrew|/usr/local' | head -20 >&2 | ||
| return 1 | ||
| fi | ||
| echo " OK: $loaded libraries loaded, none from host package prefixes" | ||
| } |
There was a problem hiding this comment.
Launch the app from the mounted DMG and verify its dyld trace does not load host package libraries.
| verify_dmg() { | ||
| local dmg="$1" mnt tmp trace marker app bin copied_app | ||
| require_macos | ||
| [[ -f "$dmg" ]] || fail "release artifact not found: $dmg" | ||
|
|
||
| note "Release artifact" | ||
| ls -lh "$dmg" | ||
| shasum -a 256 "$dmg" | ||
| hdiutil imageinfo "$dmg" >/dev/null | ||
|
|
||
| VERIFY_MNT=$(mktemp -d -t modmesh-release-dmg) | ||
| VERIFY_TMP=$(mktemp -d -t modmesh-release-app) | ||
| VERIFY_TRACE=$(mktemp -t modmesh-release-dyld) | ||
| VERIFY_MARKER=$(mktemp -t modmesh-release-marker) | ||
| mnt="$VERIFY_MNT" | ||
| tmp="$VERIFY_TMP" | ||
| trace="$VERIFY_TRACE" | ||
| marker="$VERIFY_MARKER" | ||
| trap cleanup_verify EXIT | ||
|
|
||
| note "Mounting DMG" | ||
| hdiutil attach -nobrowse -readonly -mountpoint "$mnt" "$dmg" >/dev/null | ||
| app=$(find "$mnt" -maxdepth 2 -name '*.app' -type d -print -quit) | ||
| [[ -n "$app" ]] || fail "no .app found in $dmg" | ||
| bin="$app/Contents/MacOS/$(basename "$app" .app)" | ||
|
|
||
| note "Checking mounted app structure" | ||
| verify_app_structure "$app" | ||
|
|
||
| note "Checking code signature before launch" | ||
| codesign --verify --deep --strict "$app" | ||
| spctl --assess --type execute --verbose=4 "$app" 2>&1 || \ | ||
| echo " warning: spctl rejected this app (expected for ad-hoc signed prototype builds)" | ||
|
|
||
| note "Static scan for host package paths" | ||
| check_self_contained "$app" | ||
|
|
||
| note "Runtime import check from a writable app copy" | ||
| cp -R "$app" "$tmp/" | ||
| copied_app="$tmp/$(basename "$app")" | ||
| codesign --verify --deep --strict "$copied_app" | ||
| touch "$marker" | ||
| runtime_import_check "$copied_app/Contents/MacOS/$(basename "$copied_app" .app)" | ||
| if find "$copied_app" -name '*.pyc' -newer "$marker" -print -quit | grep -q .; then | ||
| echo "ERROR: runtime import wrote Python bytecode into the signed app bundle:" >&2 | ||
| find "$copied_app" -name '*.pyc' -newer "$marker" -print | sed -n '1,20p' >&2 | ||
| return 1 | ||
| fi | ||
| codesign --verify --deep --strict "$copied_app" | ||
|
|
||
| note "Smoke launch from mounted DMG" | ||
| smoke_launch_verify "$bin" "$trace" | ||
|
|
||
| note "Release artifact verification passed" | ||
| } |
There was a problem hiding this comment.
Implement the verify subcommand by validating the generated DMG as a release artifact.
yungyuc
left a comment
There was a problem hiding this comment.
- If all does not include verify, make it explicit like.
- Discuss: I propose to use explicit names for macos:
bundle-precheck(fromrelease-check),bundle(fromrelease),bundle-test(release-test).
| Subcommands: | ||
| check Check macOS bundle release dependencies. Does not build or install. | ||
| bundle Build/package pilot.app and pilot.dmg with Homebrew dependencies. | ||
| verify Verify a generated release DMG artifact. | ||
| all Run check, then bundle. |
There was a problem hiding this comment.
If all does not include verify, make it explicit like:
Run check and then bundle. (No verify.)
| .PHONY: release-check | ||
| release-check: | ||
| $(MODMESH_ROOT)/contrib/bundle/bundle-with-homebrew.sh check | ||
|
|
||
| .PHONY: release | ||
| release: | ||
| $(MODMESH_ROOT)/contrib/bundle/bundle-with-homebrew.sh all \ | ||
| --output "$(RELEASE_OUTPUT)" $(RELEASE_ARGS) | ||
|
|
||
| .PHONY: release-test | ||
| release-test: | ||
| $(MODMESH_ROOT)/contrib/bundle/bundle-with-homebrew.sh verify \ | ||
| "$(RELEASE_ARTIFACT)" | ||
|
|
There was a problem hiding this comment.
Discuss: I propose to use explicit names for macos: bundle-precheck (from release-check), bundle (from release), bundle-test (release-test).
Add release-oriented subcommands to bundle-with-homebrew.sh: check, bundle, verify, and all. The new check step validates the Homebrew-based macOS bundling environment before packaging, while verify can inspect a generated DMG artifact.
Add Makefile entry points for the release workflow. These targets delegate to the bundle script instead of implementing release logic in Makefile:
make release now runs the check step before bundling.
Related to issue #765