Skip to content

Extend macOS bundle script with release checks#779

Merged
yungyuc merged 5 commits into
solvcon:masterfrom
iefiru:macos-release-workflow
Jun 5, 2026
Merged

Extend macOS bundle script with release checks#779
yungyuc merged 5 commits into
solvcon:masterfrom
iefiru:macos-release-workflow

Conversation

@iefiru

@iefiru iefiru commented May 14, 2026

Copy link
Copy Markdown
Collaborator

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.

Related to issue #765

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.
@yungyuc yungyuc added the build Build system and automation label May 14, 2026
@yungyuc

yungyuc commented May 14, 2026

Copy link
Copy Markdown
Member

Please use the following format to refer to the associated issue in the future:

Related to issue #765

You used "Fixes number" in the PR comment and I changed it. See Pull Request Guidelines:

When opening a pull request, reference the related issue (e.g., "Related to #725") instead of using closing keywords like "close numer", "closes number", or "fixes number". We do not let PR and commit log comments to mandate the management.

@iefiru iefiru left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added inline comments around the release workflow. Feel free to share any questions or suggestions.

Comment thread Makefile Outdated
Comment on lines +169 to +182
.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)"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • release-check checks the bundle dependencies.
  • release checks the dependencies and builds the DMG bundle.
  • release-test tests the DMG bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuss: I propose to use explicit names for macos: bundle-precheck (from release-check), bundle (from release), bundle-test (release-test).

Comment on lines +405 to +409
verify)
[[ $# -eq 1 ]] || { usage >&2; exit 2; }
VERIFY_ARTIFACT="$1"
shift
;;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MUST provide DMG file for verify.

cd "$BUNDLE_REPO_ROOT"

MIN_LOADS=${MIN_LOADS:-50}
HOST_PREFIX_RE='^(/opt/homebrew|/usr/local)'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centralize host package path patterns for easier management and reuse.

Comment thread contrib/bundle/bundle-with-homebrew.sh Outdated
Comment on lines +49 to +53
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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • check checks the bundle dependencies.
  • bundle checks the dependencies and builds the DMG bundle.
  • verify tests the DMG bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all does not include verify, make it explicit like:

Run check and then bundle. (No verify.)

Comment on lines +95 to +118
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
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import the given Python package to verify it is available.

Comment on lines +137 to +152
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
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure pybind11's CMake config is available before the build step.

Comment on lines +154 to +198
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
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement the check subcommand by running the macOS release preflight checks.

Comment on lines +269 to +278
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"
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that the DMG contains a complete macOS app bundle.

Comment on lines +288 to +317
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"
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Launch the app from the mounted DMG and verify its dyld trace does not load host package libraries.

Comment on lines +330 to +384
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"
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement the verify subcommand by validating the generated DMG as a release artifact.

@yungyuc yungyuc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • If all does not include verify, make it explicit.
  • Discuss: I propose to use explicit names for macos: bundle-precheck (from release-check), bundle (from release), bundle-test (release-test).

Comment thread contrib/bundle/bundle-with-homebrew.sh Outdated
Comment on lines +49 to +53
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all does not include verify, make it explicit like:

Run check and then bundle. (No verify.)

Comment thread Makefile Outdated
Comment on lines +169 to +182
.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)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuss: I propose to use explicit names for macos: bundle-precheck (from release-check), bundle (from release), bundle-test (release-test).

iefiru added 2 commits May 24, 2026 23:51
Rename the release-oriented make targets added for the macOS bundling
workflow to a "bundle" prefix, matching the bundle-with-homebrew.sh
subcommands they delegate to:

- make release-check -> make bundle-precheck
- make release       -> make bundle
- make release-test  -> make bundle-test
Set PYTHONDONTWRITEBYTECODE=1 before the interpreter starts so the
code-signed release bundle isn't mutated by .pyc writes at import time.

@iefiru iefiru left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update following modifications:

  • If all does not include verify, make it explicit.
  • Discuss: I propose to use explicit names for macos: bundle-precheck (from release-check), bundle (from release), bundle-test (release-test).

New Item:

  • Add the missing PYTHONDONTWRITEBYTECODE=1 setting to keep the signed bundle clean.

Comment thread cpp/modmesh/toggle/toggle.cpp Outdated
// Keep the embedded interpreter from writing .pyc files. The release app
// bundle is code-signed and ships no precompiled bytecode, so any write
// would mutate the signed bundle and break its signature.
setenv("PYTHONDONTWRITEBYTECODE", "1", 1);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disables writing Python bytecode (.pyc) at runtime so the embedded interpreter never modifies the code-signed bundle on import, which would otherwise break its signature.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike setting the Qt environment variable, disabling generation of .pyc is too offensive for a Python extension module. It should not be turned on by default.

Do it only when the process is within an app bundle.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sys.dont_write_bytecode is not preferred because it is hard to do it in Python source code before loading any modules. Please include the reason in the code comments too.

@yungyuc yungyuc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Disabling .pyc generation should not be the default behavior. The environment variable can only be set when the process is within an app bundle.

Comment thread cpp/modmesh/toggle/toggle.cpp Outdated
// Keep the embedded interpreter from writing .pyc files. The release app
// bundle is code-signed and ships no precompiled bytecode, so any write
// would mutate the signed bundle and break its signature.
setenv("PYTHONDONTWRITEBYTECODE", "1", 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike setting the Qt environment variable, disabling generation of .pyc is too offensive for a Python extension module. It should not be turned on by default.

Do it only when the process is within an app bundle.

Switch from disabling runtime bytecode writes to shipping pre-compiled
.pyc, so the embedded interpreter never mutates the signed bundle on
import.

- Stop forcing PYTHONDONTWRITEBYTECODE in the embedded interpreter
- Run compileall to pre-compile Python bytecode for the bundle

@iefiru iefiru left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yungyuc Thanks for the earlier feedback. Per our discussion, I switched the approach from forcing PYTHONDONTWRITEBYTECODE in the embedded interpreter to pre-compiling .pyc with compileall --invalidation-mode unchecked-hash before code signing. This matches what python-build-standalone / PyInstaller / py2app do, and avoids the dev-side cost of the env override.

Comment thread cpp/modmesh/toggle/toggle.cpp Outdated
Comment on lines -203 to -207
// Keep the embedded interpreter from writing .pyc files. The release app
// bundle is code-signed and ships no precompiled bytecode, so any write
// would mutate the signed bundle and break its signature.
setenv("PYTHONDONTWRITEBYTECODE", "1", 1);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert PYTHONDONTWRITEBYTECODE

Comment on lines +645 to +656
T_STEP=$SECONDS
echo "==> Pre-compiling bundled Python modules"
PY_LIB="$DEST_FW/Versions/$PY_VER/lib/python$PY_VER"
rm -rf "$PY_LIB/test"
"$DEST_FW/Versions/$PY_VER/bin/python$PY_VER" -m compileall -f \
--invalidation-mode unchecked-hash -q \
"$PY_LIB" || true
PYC_COUNT=$(find "$PY_LIB" -name '*.pyc' | wc -l | tr -d ' ')
[[ $PYC_COUNT -gt 0 ]] || \
{ echo "ERROR: compileall produced no .pyc files under $PY_LIB" >&2; exit 1; }
echo " compiled $PYC_COUNT .pyc files"
echo " [Step 5.5 (Pre-compile bytecode): $((SECONDS - T_STEP))s]"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using compileall to pre-compile python byte code for bundle.

@iefiru iefiru requested a review from yungyuc June 5, 2026 14:15

@yungyuc yungyuc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@yungyuc

yungyuc commented Jun 5, 2026

Copy link
Copy Markdown
Member

@chestercheng @ExplorerRay please take a look.

@yungyuc yungyuc merged commit 9bc7008 into solvcon:master Jun 5, 2026
17 checks passed
@yungyuc yungyuc linked an issue Jun 5, 2026 that may be closed by this pull request
@yungyuc

yungyuc commented Jun 7, 2026

Copy link
Copy Markdown
Member

I tested the script in a VM and it builds the bundle in 15 minutes. The final dmg is 300 MB. The result is reasonable.

The built DMG can run in the guest VM but not on the host. The error message:

$ pilot.app/Contents/MacOS/pilot
Can not find thirdparty.
ModuleNotFoundError: No module named 'six'

At:
...

Let's fix it in a followup PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build system and automation

Projects

Development

Successfully merging this pull request may close these issues.

Develop a make target to test for releasing pilot

2 participants