1#!/usr/bin/env bash
2set -euo pipefail
3
4# Script to package jacquard-lexicon binaries for distribution using Nix cross-compilation
5# Creates tar.xz archives with binaries, man pages, completions, README, LICENSE, and config
6#
7# Generates two versions:
8# - Unversioned archives in binaries/ (tracked in git, overwritten each build)
9# - Versioned archives in binaries/releases/ (gitignored, for GitHub releases)
10
11# Determine project root (script is in scripts/ subdirectory)
12SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
14cd "$PROJECT_ROOT"
15
16# Parse version from workspace Cargo.toml
17VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
18echo "Packaging version: $VERSION"
19
20# Detect current system
21CURRENT_SYSTEM=$(nix eval --impure --expr 'builtins.currentSystem' --raw)
22echo "Current system: $CURRENT_SYSTEM"
23
24# Map target triples to nix package names
25declare -A TARGET_TO_PACKAGE=(
26 ["x86_64-unknown-linux-gnu"]="jacquard-lexgen-x86_64-linux"
27 ["aarch64-unknown-linux-gnu"]="jacquard-lexgen-aarch64-linux"
28 ["x86_64-apple-darwin"]="jacquard-lexgen-x86_64-darwin"
29 ["aarch64-apple-darwin"]="jacquard-lexgen-aarch64-darwin"
30 ["x86_64-pc-windows-gnu"]="jacquard-lexgen-x86_64-windows"
31 ["aarch64-pc-windows-gnullvm"]="jacquard-lexgen-aarch64-windows"
32)
33
34# Determine which targets we can build from the current system
35TARGETS=()
36case "$CURRENT_SYSTEM" in
37 x86_64-linux)
38 # Linux can cross-compile to other Linux archs and Windows x86_64
39 # macOS requires remote builders or actual macOS hardware
40 # aarch64-windows: nixpkgs mingw-w64-pthreads broken for aarch64 (missing winver.h)
41 TARGETS=(
42 "x86_64-unknown-linux-gnu"
43 "aarch64-unknown-linux-gnu"
44 "x86_64-pc-windows-gnu"
45 # "aarch64-pc-windows-gnullvm" # TODO: nixpkgs cross-compile broken
46 )
47 echo "Building from x86_64-linux: Linux (x86_64, aarch64) + Windows (x86_64)"
48 echo "Note: macOS cross-compilation requires remote builders or macOS hardware"
49 echo "Note: aarch64-windows cross-compilation broken in nixpkgs (mingw-w64-pthreads build fails)"
50 ;;
51 aarch64-linux)
52 # Linux can cross-compile to other Linux archs and Windows x86_64
53 # macOS requires remote builders or actual macOS hardware
54 # aarch64-windows: nixpkgs mingw-w64-pthreads broken for aarch64 (missing winver.h)
55 TARGETS=(
56 "aarch64-unknown-linux-gnu"
57 "x86_64-unknown-linux-gnu"
58 "x86_64-pc-windows-gnu"
59 # "aarch64-pc-windows-gnullvm" # TODO: nixpkgs cross-compile broken
60 )
61 echo "Building from aarch64-linux: Linux (aarch64, x86_64) + Windows (x86_64)"
62 echo "Note: macOS cross-compilation requires remote builders or macOS hardware"
63 echo "Note: aarch64-windows cross-compilation broken in nixpkgs (mingw-w64-pthreads build fails)"
64 ;;
65 x86_64-darwin)
66 # macOS cross-compilation is limited
67 TARGETS=(
68 "x86_64-apple-darwin"
69 )
70 echo "Building from x86_64-darwin: x86_64-darwin only"
71 ;;
72 aarch64-darwin)
73 # macOS aarch64 can build both macOS targets via rosetta
74 TARGETS=(
75 "aarch64-apple-darwin"
76 "x86_64-apple-darwin"
77 )
78 echo "Building from aarch64-darwin: macOS targets (aarch64 + x86_64 via rosetta)"
79 ;;
80 *)
81 echo "Error: Unknown system: $CURRENT_SYSTEM"
82 echo "This script supports: x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin"
83 exit 1
84 ;;
85esac
86
87echo ""
88echo "Will build for: ${TARGETS[*]}"
89echo ""
90
91# Output directories
92OUTPUT_DIR="binaries"
93RELEASES_DIR="binaries/releases"
94mkdir -p "$OUTPUT_DIR"
95mkdir -p "$RELEASES_DIR"
96
97# Helper function to package for a target
98package_target() {
99 local TARGET=$1
100 local PACKAGE_NAME="${TARGET_TO_PACKAGE[$TARGET]}"
101
102 echo ""
103 echo "======================================"
104 echo "Building for $TARGET"
105 echo "======================================"
106
107 # Build with nix using cross-compilation package
108 echo "Running: nix build .#${PACKAGE_NAME}"
109 if ! nix build ".#${PACKAGE_NAME}" -o "result-${TARGET}"; then
110 echo "Error: nix build failed for $TARGET"
111 return 1
112 fi
113
114 # Package each binary separately
115 for BINARY in lex-fetch jacquard-codegen; do
116 echo " Packaging binary: $BINARY"
117 package_binary "$TARGET" "$BINARY"
118 done
119
120 # Cleanup
121 rm -f "$PROJECT_ROOT/result-${TARGET}"
122 cd "$PROJECT_ROOT"
123}
124
125# Helper function to package a single binary
126package_binary() {
127 local TARGET=$1
128 local BINARY=$2
129
130 # Determine binary extension
131 local BINARY_EXT=""
132 if [[ "$TARGET" == *"windows"* ]]; then
133 BINARY_EXT=".exe"
134 fi
135
136 # Check if binary exists
137 if [[ ! -f "result-${TARGET}/bin/${BINARY}${BINARY_EXT}" ]]; then
138 echo " Warning: ${BINARY}${BINARY_EXT} not found, skipping"
139 return 0
140 fi
141
142 # Names for versioned and unversioned archives
143 local VERSIONED_NAME="${BINARY}_${TARGET}_v${VERSION}"
144 local UNVERSIONED_NAME="${BINARY}_${TARGET}"
145
146 # Create staging directory
147 local STAGE_DIR="/tmp/${VERSIONED_NAME}"
148 rm -rf "$STAGE_DIR"
149 mkdir -p "$STAGE_DIR"
150
151 # Detect if this is a Windows target
152 if [[ "$TARGET" == *"windows"* ]]; then
153 # Windows: binary, README, LICENSE, example config (for lex-fetch only)
154 cp "result-${TARGET}/bin/${BINARY}.exe" "$STAGE_DIR/"
155 cp LICENSE "$STAGE_DIR/"
156 cp README.md "$STAGE_DIR/"
157
158 # Only include example config for lex-fetch
159 if [[ "$BINARY" == "lex-fetch" ]]; then
160 mkdir -p "$STAGE_DIR/examples"
161 cp crates/jacquard-lexgen/lexicons.kdl.example "$STAGE_DIR/examples/" 2>/dev/null || true
162 fi
163 else
164 # Unix (Linux/macOS): binary, man page, completions, README, LICENSE
165 mkdir -p "$STAGE_DIR/bin"
166 cp "result-${TARGET}/bin/${BINARY}" "$STAGE_DIR/bin/"
167
168 cp LICENSE "$STAGE_DIR/"
169 cp README.md "$STAGE_DIR/"
170
171 # Copy man page if it exists
172 if [[ -f "result-${TARGET}/share/man/man1/${BINARY}.1.gz" ]]; then
173 mkdir -p "$STAGE_DIR/share/man/man1"
174 cp "result-${TARGET}/share/man/man1/${BINARY}.1.gz" "$STAGE_DIR/share/man/man1/"
175 fi
176
177 # Copy completions if they exist
178 for shell_dir in bash fish zsh; do
179 local comp_dir="result-${TARGET}/share/$shell_dir/site-functions"
180 if [[ "$shell_dir" == "bash" ]]; then
181 comp_dir="result-${TARGET}/share/bash-completion/completions"
182 elif [[ "$shell_dir" == "fish" ]]; then
183 comp_dir="result-${TARGET}/share/fish/vendor_completions.d"
184 fi
185
186 if [[ -d "$comp_dir" ]]; then
187 for comp in "$comp_dir"/*; do
188 local comp_name=$(basename "$comp")
189 # Only copy completions for this specific binary
190 if [[ "$comp_name" == "${BINARY}"* ]] || [[ "$comp_name" == "_${BINARY}" ]]; then
191 mkdir -p "$STAGE_DIR/share/$(dirname "${comp#result-${TARGET}/share/}")"
192 cp "$comp" "$STAGE_DIR/share/$(dirname "${comp#result-${TARGET}/share/}")/"
193 fi
194 done
195 fi
196 done
197
198 # Only include example config for lex-fetch
199 if [[ "$BINARY" == "lex-fetch" ]]; then
200 mkdir -p "$STAGE_DIR/share/doc/jacquard-lexicon"
201 cp crates/jacquard-lexgen/lexicons.kdl.example "$STAGE_DIR/share/doc/jacquard-lexgen/" 2>/dev/null || true
202 fi
203 fi
204
205 # Create versioned archive (for releases)
206 cd /tmp
207
208 # Use .zip for Windows, .tar.xz for Unix
209 if [[ "$TARGET" == *"windows"* ]]; then
210 zip -r "${VERSIONED_NAME}.zip" "$VERSIONED_NAME"
211 mv "${VERSIONED_NAME}.zip" "$PROJECT_ROOT/$RELEASES_DIR/"
212 echo " Created: ${RELEASES_DIR}/${VERSIONED_NAME}.zip"
213
214 # Rename and create unversioned archive
215 mv "$VERSIONED_NAME" "$UNVERSIONED_NAME"
216 zip -r "${UNVERSIONED_NAME}.zip" "$UNVERSIONED_NAME"
217 mv "${UNVERSIONED_NAME}.zip" "$PROJECT_ROOT/$OUTPUT_DIR/"
218 echo " Created: ${OUTPUT_DIR}/${UNVERSIONED_NAME}.zip"
219 else
220 tar -cJf "${VERSIONED_NAME}.tar.xz" "$VERSIONED_NAME"
221 mv "${VERSIONED_NAME}.tar.xz" "$PROJECT_ROOT/$RELEASES_DIR/"
222 echo " Created: ${RELEASES_DIR}/${VERSIONED_NAME}.tar.xz"
223
224 # Rename and create unversioned archive
225 mv "$VERSIONED_NAME" "$UNVERSIONED_NAME"
226 tar -cJf "${UNVERSIONED_NAME}.tar.xz" "$UNVERSIONED_NAME"
227 mv "${UNVERSIONED_NAME}.tar.xz" "$PROJECT_ROOT/$OUTPUT_DIR/"
228 echo " Created: ${OUTPUT_DIR}/${UNVERSIONED_NAME}.tar.xz"
229 fi
230
231 # Cleanup
232 rm -rf "$UNVERSIONED_NAME"
233
234 # Return to project root
235 cd "$PROJECT_ROOT"
236}
237
238# Build for all targets
239for target in "${TARGETS[@]}"; do
240 package_target "$target" || echo "Warning: build failed for $target, continuing..."
241done
242
243# Print summary
244echo ""
245echo "Packaging complete!"
246echo ""
247echo "Tracked archives (binaries/):"
248ls -lh "$OUTPUT_DIR"/*.tar.xz 2>/dev/null || true
249ls -lh "$OUTPUT_DIR"/*.zip 2>/dev/null || true
250echo ""
251echo "Release archives (binaries/releases/):"
252ls -lh "$RELEASES_DIR"/*.tar.xz 2>/dev/null || true
253ls -lh "$RELEASES_DIR"/*.zip 2>/dev/null || true
254
255# Generate checksums for tracked archives
256echo ""
257echo "Generating checksums for tracked archives..."
258cd "$OUTPUT_DIR"
259sha256sum *.tar.xz *.zip 2>/dev/null > SHA256SUMS || true
260echo "Checksums written to ${OUTPUT_DIR}/SHA256SUMS"
261cat SHA256SUMS
262
263# Generate checksums for release archives
264echo ""
265echo "Generating checksums for release archives..."
266cd "$PROJECT_ROOT/$RELEASES_DIR"
267sha256sum *.tar.xz *.zip 2>/dev/null > SHA256SUMS || true
268echo "Checksums written to ${RELEASES_DIR}/SHA256SUMS"
269cat SHA256SUMS