Skip to content

Commit fadb574

Browse files
committed
Add macOS packaging script and docs
Add scripts/package-macos.sh and update README.md with macOS packaging instructions. The script builds (optional), signs, bundles Homebrew-linked dylibs, rewrites install names, and creates a zip at dist/comon-v<version>-<apple-target>.zip. It also produces an install.sh and README.txt inside the package and can submit the ZIP to Apple notarization via xcrun notarytool using NOTARY_PROFILE; SKIP_NOTARY=1 or an unset SIGN_IDENTITY will use ad-hoc signing. README examples document common invocation patterns and warn not to commit signing/notary credentials. Options: --target, --out-dir, --skip-build.
1 parent e3a3456 commit fadb574

2 files changed

Lines changed: 379 additions & 0 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,29 @@ bash scripts/package-prebuilt.sh --musl
262262
bash scripts/package-prebuilt.sh --gnu
263263
```
264264

265+
macOS signed release package:
266+
267+
```bash
268+
# Local verification package with ad-hoc signing and no notarization:
269+
SKIP_NOTARY=1 bash scripts/package-macos.sh
270+
271+
# Developer ID signed package, submitted with a stored notarytool profile:
272+
SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
273+
NOTARY_PROFILE="comon-notary" \
274+
bash scripts/package-macos.sh
275+
276+
# Optional explicit target:
277+
bash scripts/package-macos.sh --target aarch64-apple-darwin
278+
```
279+
280+
The macOS script builds `comon`, signs the executable, bundles Homebrew-linked
281+
dylibs into the package when needed, and creates:
282+
283+
- `dist/comon-v<version>-<apple-target>.zip`
284+
285+
Signing identity and notarization profile values are read from environment
286+
variables only; do not commit credentials or Apple account details into the repo.
287+
265288
Package output:
266289

267290
- `dist/comon-v<version>-<target>.zip`

scripts/package-macos.sh

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
print_usage() {
5+
cat <<'EOF'
6+
Usage: package-macos.sh [--target <triple>] [--out-dir <dir>] [--skip-build]
7+
8+
Builds, signs, and packages a macOS comon release zip.
9+
10+
Environment:
11+
SIGN_IDENTITY Developer ID Application identity. Defaults to ad-hoc signing.
12+
NOTARY_PROFILE notarytool keychain profile name. Required unless SKIP_NOTARY=1.
13+
SKIP_NOTARY Set to 1 to skip notarization.
14+
15+
Options:
16+
--target <triple> Rust Apple target triple to build/package.
17+
--out-dir <dir> Output directory for package zip (default: dist).
18+
--skip-build Package existing binary without running cargo build.
19+
-h, --help Show this help.
20+
EOF
21+
}
22+
23+
TARGET=""
24+
OUT_DIR="dist"
25+
SKIP_BUILD=0
26+
27+
while [ $# -gt 0 ]; do
28+
case "$1" in
29+
--target)
30+
if [ $# -lt 2 ]; then
31+
echo "Missing value for --target" >&2
32+
exit 1
33+
fi
34+
TARGET="$2"
35+
shift 2
36+
;;
37+
--out-dir)
38+
if [ $# -lt 2 ]; then
39+
echo "Missing value for --out-dir" >&2
40+
exit 1
41+
fi
42+
OUT_DIR="$2"
43+
shift 2
44+
;;
45+
--skip-build)
46+
SKIP_BUILD=1
47+
shift
48+
;;
49+
-h|--help)
50+
print_usage
51+
exit 0
52+
;;
53+
*)
54+
echo "Unknown option: $1" >&2
55+
print_usage >&2
56+
exit 1
57+
;;
58+
esac
59+
done
60+
61+
if [ "$(uname -s)" != "Darwin" ]; then
62+
echo "macOS packaging must run on macOS." >&2
63+
exit 1
64+
fi
65+
66+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
67+
REPO_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
68+
DIST_DIR="${REPO_DIR}/${OUT_DIR}"
69+
SIGN_IDENTITY="${SIGN_IDENTITY:-}"
70+
NOTARY_PROFILE="${NOTARY_PROFILE:-}"
71+
SKIP_NOTARY="${SKIP_NOTARY:-0}"
72+
73+
if ! command -v cargo >/dev/null 2>&1; then
74+
echo "cargo not found in PATH. Install Rust first: https://rustup.rs" >&2
75+
exit 1
76+
fi
77+
if ! command -v rustc >/dev/null 2>&1; then
78+
echo "rustc not found in PATH. Install Rust first: https://rustup.rs" >&2
79+
exit 1
80+
fi
81+
if ! command -v ditto >/dev/null 2>&1; then
82+
echo "ditto not found in PATH. Install Xcode command line tools." >&2
83+
exit 1
84+
fi
85+
if ! command -v otool >/dev/null 2>&1; then
86+
echo "otool not found in PATH. Install Xcode command line tools." >&2
87+
exit 1
88+
fi
89+
if ! command -v install_name_tool >/dev/null 2>&1; then
90+
echo "install_name_tool not found in PATH. Install Xcode command line tools." >&2
91+
exit 1
92+
fi
93+
if ! command -v codesign >/dev/null 2>&1; then
94+
echo "codesign not found in PATH. Install Xcode command line tools." >&2
95+
exit 1
96+
fi
97+
98+
HOST_TRIPLE="$(rustc -vV | sed -n 's/^host: //p')"
99+
if [ -z "${HOST_TRIPLE}" ]; then
100+
echo "Unable to detect Rust host target from rustc." >&2
101+
exit 1
102+
fi
103+
104+
if [ -z "${TARGET}" ]; then
105+
TARGET="${HOST_TRIPLE}"
106+
fi
107+
case "${TARGET}" in
108+
*-apple-darwin) ;;
109+
*)
110+
echo "macOS packaging requires an Apple Darwin target, got: ${TARGET}" >&2
111+
exit 1
112+
;;
113+
esac
114+
115+
if [ "${TARGET}" != "${HOST_TRIPLE}" ]; then
116+
if ! command -v rustup >/dev/null 2>&1; then
117+
echo "rustup is required to add target ${TARGET}. Install via https://rustup.rs" >&2
118+
exit 1
119+
fi
120+
rustup target add "${TARGET}"
121+
fi
122+
123+
if [ -z "${SIGN_IDENTITY}" ]; then
124+
SIGN_IDENTITY="-"
125+
SKIP_NOTARY=1
126+
echo "SIGN_IDENTITY is not set; using ad-hoc signing and skipping notarization." >&2
127+
fi
128+
if [ "${SKIP_NOTARY}" != "1" ] && [ -z "${NOTARY_PROFILE}" ]; then
129+
echo "NOTARY_PROFILE is required unless SKIP_NOTARY=1." >&2
130+
echo "Create one with: xcrun notarytool store-credentials <profile-name> --apple-id <email> --team-id <team-id>" >&2
131+
exit 1
132+
fi
133+
if [ "${SIGN_IDENTITY}" = "-" ]; then
134+
SIGN_TIMESTAMP_ARGS=(--timestamp=none)
135+
else
136+
SIGN_TIMESTAMP_ARGS=(--timestamp)
137+
fi
138+
139+
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "${REPO_DIR}/Cargo.toml" | head -n1)"
140+
if [ -z "${VERSION}" ]; then
141+
echo "Unable to read version from Cargo.toml" >&2
142+
exit 1
143+
fi
144+
145+
BUILD_ARGS=(--release)
146+
if [ "${TARGET}" != "${HOST_TRIPLE}" ]; then
147+
BUILD_ARGS+=(--target "${TARGET}")
148+
fi
149+
if [ "${SKIP_BUILD}" -eq 0 ]; then
150+
(
151+
cd "${REPO_DIR}"
152+
cargo build "${BUILD_ARGS[@]}"
153+
)
154+
fi
155+
156+
BIN_SRC="${REPO_DIR}/target/${TARGET}/release/comon"
157+
if [ "${TARGET}" = "${HOST_TRIPLE}" ] && [ ! -f "${BIN_SRC}" ]; then
158+
BIN_SRC="${REPO_DIR}/target/release/comon"
159+
fi
160+
if [ ! -f "${BIN_SRC}" ]; then
161+
echo "Built binary not found: ${BIN_SRC}" >&2
162+
exit 1
163+
fi
164+
165+
PKG_BASE="comon-v${VERSION}-${TARGET}"
166+
PKG_ROOT="${DIST_DIR}/${PKG_BASE}"
167+
ZIP_PATH="${DIST_DIR}/${PKG_BASE}.zip"
168+
QUEUE_FILE="${DIST_DIR}/dylib-queue.txt"
169+
PROCESSED_FILE="${DIST_DIR}/dylib-processed.txt"
170+
171+
cleanup() {
172+
rm -f "${QUEUE_FILE}" "${PROCESSED_FILE}"
173+
}
174+
trap cleanup EXIT
175+
176+
is_bundled_dependency() {
177+
case "$1" in
178+
/opt/homebrew/*|/usr/local/*)
179+
return 0
180+
;;
181+
*)
182+
return 1
183+
;;
184+
esac
185+
}
186+
187+
dependency_basename() {
188+
basename "$1"
189+
}
190+
191+
collect_bundled_dependencies() {
192+
otool -L "$1" | awk 'NR > 1 { print $1 }' | while IFS= read -r dep; do
193+
if is_bundled_dependency "${dep}"; then
194+
printf '%s\n' "${dep}"
195+
fi
196+
done
197+
}
198+
199+
queue_dependency() {
200+
if ! grep -Fxq "$1" "${QUEUE_FILE}"; then
201+
printf '%s\n' "$1" >> "${QUEUE_FILE}"
202+
fi
203+
}
204+
205+
rewrite_dependency_references() {
206+
local binary="$1"
207+
local dep_prefix="$2"
208+
209+
collect_bundled_dependencies "${binary}" | while IFS= read -r dep; do
210+
install_name_tool -change "${dep}" "${dep_prefix}/$(dependency_basename "${dep}")" "${binary}"
211+
done
212+
}
213+
214+
bundle_dylibs() {
215+
: > "${QUEUE_FILE}"
216+
: > "${PROCESSED_FILE}"
217+
218+
collect_bundled_dependencies "${PKG_ROOT}/comon" | while IFS= read -r dep; do
219+
queue_dependency "${dep}"
220+
done
221+
222+
while IFS= read -r dep; do
223+
if grep -Fxq "${dep}" "${PROCESSED_FILE}"; then
224+
continue
225+
fi
226+
227+
local dest
228+
dest="${PKG_ROOT}/lib/$(dependency_basename "${dep}")"
229+
echo "Bundling dylib: ${dep}"
230+
mkdir -p "${PKG_ROOT}/lib"
231+
ditto --noextattr --noacl "${dep}" "${dest}"
232+
chmod u+w "${dest}"
233+
install_name_tool -id "@loader_path/$(dependency_basename "${dep}")" "${dest}"
234+
235+
collect_bundled_dependencies "${dest}" | while IFS= read -r nested_dep; do
236+
queue_dependency "${nested_dep}"
237+
done
238+
239+
printf '%s\n' "${dep}" >> "${PROCESSED_FILE}"
240+
done < "${QUEUE_FILE}"
241+
242+
rewrite_dependency_references "${PKG_ROOT}/comon" "@executable_path/lib"
243+
if [ -d "${PKG_ROOT}/lib" ]; then
244+
find "${PKG_ROOT}/lib" -type f -name '*.dylib' -print | while IFS= read -r dylib; do
245+
rewrite_dependency_references "${dylib}" "@loader_path"
246+
done
247+
fi
248+
}
249+
250+
sign_binary() {
251+
codesign --force \
252+
--sign "${SIGN_IDENTITY}" \
253+
--options runtime \
254+
"${SIGN_TIMESTAMP_ARGS[@]}" \
255+
"$1"
256+
}
257+
258+
rm -rf "${PKG_ROOT}"
259+
mkdir -p "${PKG_ROOT}" "${DIST_DIR}"
260+
261+
ditto --noextattr --noacl "${BIN_SRC}" "${PKG_ROOT}/comon"
262+
chmod 755 "${PKG_ROOT}/comon"
263+
ditto --noextattr --noacl "${REPO_DIR}/LICENSE" "${PKG_ROOT}/LICENSE"
264+
265+
bundle_dylibs
266+
267+
if [ -d "${PKG_ROOT}/lib" ]; then
268+
find "${PKG_ROOT}/lib" -type f -name '*.dylib' -print | while IFS= read -r dylib; do
269+
sign_binary "${dylib}"
270+
done
271+
fi
272+
sign_binary "${PKG_ROOT}/comon"
273+
codesign --verify --strict --verbose=4 "${PKG_ROOT}/comon"
274+
275+
cat > "${PKG_ROOT}/install.sh" <<'EOF'
276+
#!/usr/bin/env bash
277+
set -euo pipefail
278+
279+
ROOT="${1:-$HOME/.local}"
280+
PKG_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
281+
SRC_BIN="${PKG_DIR}/comon"
282+
COMON_HOME_DIR="${COMON_HOME:-$HOME/.comon}"
283+
284+
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
285+
echo "Usage: install.sh [root]"
286+
echo "Installs comon into <root>/bin and bundled dylibs into <root>/bin/lib."
287+
exit 0
288+
fi
289+
if [ $# -gt 1 ]; then
290+
echo "Too many arguments" >&2
291+
exit 1
292+
fi
293+
if [ ! -f "${SRC_BIN}" ]; then
294+
echo "Missing binary: ${SRC_BIN}" >&2
295+
exit 1
296+
fi
297+
if [ -L "${COMON_HOME_DIR}" ]; then
298+
echo "Refusing to use COMON_HOME (${COMON_HOME_DIR}): symlink is not allowed." >&2
299+
exit 1
300+
fi
301+
if [ -e "${COMON_HOME_DIR}" ] && [ ! -d "${COMON_HOME_DIR}" ]; then
302+
echo "Refusing to use COMON_HOME (${COMON_HOME_DIR}): expected a directory." >&2
303+
exit 1
304+
fi
305+
306+
mkdir -p "${ROOT}/bin" "${COMON_HOME_DIR}"
307+
chmod 700 "${COMON_HOME_DIR}" 2>/dev/null || true
308+
install -m 755 "${SRC_BIN}" "${ROOT}/bin/comon"
309+
310+
if [ -d "${PKG_DIR}/lib" ]; then
311+
mkdir -p "${ROOT}/bin/lib"
312+
find "${PKG_DIR}/lib" -type f -name '*.dylib' -print | while IFS= read -r dylib; do
313+
install -m 755 "${dylib}" "${ROOT}/bin/lib/$(basename "${dylib}")"
314+
done
315+
fi
316+
317+
echo "Installed comon to ${ROOT}/bin/comon"
318+
echo "Prepared COMON_HOME at ${COMON_HOME_DIR}"
319+
case ":${PATH}:" in
320+
*":${ROOT}/bin:"*) ;;
321+
*) echo "Add to PATH: export PATH=\"${ROOT}/bin:\$PATH\"" ;;
322+
esac
323+
EOF
324+
chmod 755 "${PKG_ROOT}/install.sh"
325+
326+
cat > "${PKG_ROOT}/README.txt" <<EOF
327+
comon ${VERSION} (${TARGET})
328+
329+
Run from this package:
330+
./comon
331+
332+
Install (user scope, no Cargo required):
333+
bash install.sh
334+
335+
Optional custom install root:
336+
bash install.sh ~/.local
337+
338+
Binary path after install:
339+
~/.local/bin/comon
340+
EOF
341+
342+
rm -f "${ZIP_PATH}"
343+
(
344+
cd "${DIST_DIR}"
345+
ditto -c -k --keepParent "${PKG_BASE}" "${ZIP_PATH}"
346+
)
347+
348+
if [ "${SKIP_NOTARY}" != "1" ]; then
349+
xcrun notarytool submit "${ZIP_PATH}" \
350+
--keychain-profile "${NOTARY_PROFILE}" \
351+
--wait
352+
else
353+
echo "Skipping notarization because SKIP_NOTARY=1."
354+
fi
355+
356+
echo "Created: ${ZIP_PATH}"

0 commit comments

Comments
 (0)