返回 CodeWhale
create-release-bundles.sh
根目录 / scripts / release / create-release-bundles.sh
1 #!/usr/bin/env bash
2 set -euo pipefail
3
4 if [[ $# -ne 2 ]]; then
5 echo "usage: $0 INPUT_ARTIFACT_DIR OUTPUT_BUNDLE_DIR" >&2
6 exit 2
7 fi
8
9 artifact_dir="$1"
10 bundle_dir="$2"
11
12 # Archive metadata must be stable across recovery builds. The public workflow
13 # still refuses to replace existing release assets; reproducible packaging is a
14 # diagnostic and provenance aid, not permission to overwrite published bytes.
15 export TZ=UTC
16 if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then
17 echo "SOURCE_DATE_EPOCH is required; set it to the tagged/source commit timestamp" >&2
18 exit 1
19 fi
20 if ! [[ "${SOURCE_DATE_EPOCH}" =~ ^[0-9]+$ ]]; then
21 echo "SOURCE_DATE_EPOCH must be an integer Unix timestamp, got: ${SOURCE_DATE_EPOCH}" >&2
22 exit 1
23 fi
24
25 # Trim leading zeroes before the length and range checks, avoiding arithmetic
26 # overflow from malformed values. ZIP stores DOS timestamps, whose valid range
27 # is 1980-01-01T00:00:00Z through 2107-12-31T23:59:58Z.
28 source_date_epoch="${SOURCE_DATE_EPOCH#"${SOURCE_DATE_EPOCH%%[!0]*}"}"
29 source_date_epoch="${source_date_epoch:-0}"
30 if (( ${#source_date_epoch} > 10 )) ||
31 (( 10#${source_date_epoch} < 315532800 || 10#${source_date_epoch} > 4354819198 )); then
32 echo "SOURCE_DATE_EPOCH must be between 315532800 (1980-01-01T00:00:00Z) and 4354819198 (2107-12-31T23:59:58Z) for ZIP archives" >&2
33 exit 1
34 fi
35 source_date_epoch="$((10#${source_date_epoch}))"
36
37 if date --version >/dev/null 2>&1; then
38 archive_timestamp="$(date -u -d "@${source_date_epoch}" '+%Y%m%d%H%M.%S')"
39 else
40 archive_timestamp="$(date -u -r "${source_date_epoch}" '+%Y%m%d%H%M.%S')"
41 fi
42
43 if [[ ! -d "${artifact_dir}" ]]; then
44 echo "input artifact directory does not exist: ${artifact_dir}" >&2
45 exit 1
46 fi
47 artifact_dir="$(cd "${artifact_dir}" && pwd)"
48
49 if [[ -e "${bundle_dir}" && ! -d "${bundle_dir}" ]]; then
50 echo "output bundle path is not a directory: ${bundle_dir}" >&2
51 exit 1
52 fi
53 if [[ -e "${bundle_dir}" && -n "$(find "${bundle_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
54 echo "output bundle directory must be empty: ${bundle_dir}" >&2
55 exit 1
56 fi
57 mkdir -p "${bundle_dir}"
58 bundle_dir="$(cd "${bundle_dir}" && pwd)"
59
60 manifest="${bundle_dir}/codewhale-bundles-sha256.txt"
61 : > "${manifest}"
62
63 # Windows archives must contain CRLF batch files regardless of the builder's
64 # working-tree line endings (`* text=auto` checks out LF on macOS/Linux).
65 write_crlf_file() {
66 local src="$1"
67 local dest="$2"
68 if [[ ! -f "${src}" ]]; then
69 echo "missing Windows launcher or install script: ${src}" >&2
70 exit 1
71 fi
72 awk '{ sub(/\r$/, ""); printf "%s\r\n", $0 }' "${src}" > "${dest}"
73 }
74
75 bundle() {
76 local platform="$1"
77 local cli_src="$2"
78 local shim_src="$3"
79 local ext="$4"
80 local variant="$5"
81
82 local stem="codewhale-${platform}${variant:+-}${variant}"
83 local cli_dst="codewhale"
84 local shim_dst="codew"
85 if [[ "${platform}" == windows-* ]]; then
86 cli_dst="codewhale.exe"
87 shim_dst="codew.exe"
88 fi
89
90 local cli_path="${artifact_dir}/${cli_src}/${cli_src}"
91 local shim_path="${artifact_dir}/${shim_src}/${shim_src}"
92 if [[ ! -f "${cli_path}" ]]; then
93 echo "missing required release artifact for ${platform}: ${cli_path}" >&2
94 exit 1
95 fi
96 if [[ ! -f "${shim_path}" ]]; then
97 echo "missing required release artifact for ${platform}: ${shim_path}" >&2
98 exit 1
99 fi
100
101 local stage_root
102 stage_root="$(mktemp -d)"
103 local stage_dir="${stage_root}/${stem}"
104 mkdir -p "${stage_dir}"
105
106 cp "${cli_path}" "${stage_dir}/${cli_dst}"
107 cp "${shim_path}" "${stage_dir}/${shim_dst}"
108
109 # Optional images are projected from the same qualified, hash-checked
110 # catalog. Android and targets without a receipt remain CLI-only.
111 local host_dir="${artifact_dir}/codewhale-compiled-hosts"
112 if [[ -f "${host_dir}/codewhale-extension-hosts.json" ]]; then
113 node - "${host_dir}" "${stage_dir}" "${platform}" <<'NODE'
114 const fs = require('node:fs');
115 const path = require('node:path');
116 const hosts = require('./npm/codewhale/scripts/compiled-hosts');
117 const [directory, stage, target] = process.argv.slice(2);
118 const catalog = hosts.parseCatalog(fs.readFileSync(path.join(directory, hosts.HOST_CATALOG), 'utf8'));
119 const host = catalog.hosts.find((entry) => entry.target === target);
120 if (host) {
121 hosts.verifyDirectory(directory, { ...catalog, hosts: [host] });
122 const ext = target.startsWith('windows-') ? '.exe' : '';
123 for (const [asset, name] of [[host.asset, hosts.HOST_NAME + ext], [host.notices_asset, hosts.HOST_NAME + '.LICENSES.txt'], [host.source_asset, hosts.HOST_NAME + '.relink-source.tar.gz']]) fs.copyFileSync(path.join(directory, asset), path.join(stage, name));
124 fs.writeFileSync(path.join(stage, hosts.HOST_NAME + '.release.json'), JSON.stringify(catalog, null, 2) + '\n');
125 if (!ext) fs.chmodSync(path.join(stage, hosts.HOST_NAME), 0o755);
126 }
127 NODE
128 fi
129
130 # actions/upload-artifact intentionally normalizes downloaded files to 0644.
131 # Restore the executable contract before constructing Unix archives.
132 if [[ "${platform}" != windows-* ]]; then
133 chmod 0755 \
134 "${stage_dir}/${cli_dst}" \
135 "${stage_dir}/${shim_dst}"
136 fi
137
138 # Regular and portable Windows zips ship the Terminal-aware launcher (#1854).
139 # The GitHub flat asset `codewhale.bat` still targets the x64 release filename;
140 # archives rename the binary to codewhale.exe, so they reuse the NSIS launcher.
141 if [[ "${platform}" == windows-* ]]; then
142 write_crlf_file \
143 scripts/installer/codewhale.bat \
144 "${stage_dir}/codewhale.bat"
145 fi
146
147 if [[ "${variant}" != "portable" ]]; then
148 if [[ "${platform}" == windows-* ]]; then
149 write_crlf_file \
150 scripts/release/install.bat \
151 "${stage_dir}/install.bat"
152 write_crlf_file scripts/release/install.ps1 "${stage_dir}/install.ps1"
153 else
154 cp scripts/release/install.sh "${stage_dir}/"
155 chmod +x "${stage_dir}/install.sh"
156 fi
157 fi
158
159 # zip and tar both record mtimes; normalize every staged entry to the exact
160 # source commit timestamp so identical inputs do not produce checksum drift.
161 find "${stage_dir}" -exec touch -t "${archive_timestamp}" {} +
162
163 local archive="${bundle_dir}/${stem}.${ext}"
164 if [[ "${ext}" == "zip" ]]; then
165 (cd "${stage_root}" && zip -Xqr "${archive}" "${stem}/")
166 elif tar --version 2>/dev/null | grep -q 'GNU tar'; then
167 tar \
168 --sort=name \
169 --mtime="@${source_date_epoch}" \
170 --owner=0 \
171 --group=0 \
172 --numeric-owner \
173 --format=ustar \
174 -cf - \
175 -C "${stage_root}" \
176 "${stem}/" | gzip -n > "${archive}"
177 else
178 COPYFILE_DISABLE=1 tar -cf - -C "${stage_root}" "${stem}/" | gzip -n > "${archive}"
179 fi
180
181 local checksum
182 checksum="$(sha256sum "${archive}" | awk '{print $1}')"
183 printf '%s %s\n' "${checksum}" "$(basename "${archive}")" >> "${manifest}"
184 rm -rf "${stage_root}"
185 echo "Created ${archive}"
186 }
187
188 bundle linux-x64 \
189 codewhale-linux-x64 codew-linux-x64 tar.gz ""
190 bundle linux-arm64 \
191 codewhale-linux-arm64 codew-linux-arm64 tar.gz ""
192 bundle android-arm64 \
193 codewhale-android-arm64 codew-android-arm64 tar.gz ""
194 bundle macos-x64 \
195 codewhale-macos-x64 codew-macos-x64 tar.gz ""
196 bundle macos-arm64 \
197 codewhale-macos-arm64 codew-macos-arm64 tar.gz ""
198 bundle windows-x64 \
199 codewhale-windows-x64.exe codew-windows-x64.exe zip ""
200 bundle windows-x64 \
201 codewhale-windows-x64.exe codew-windows-x64.exe zip portable
202 bundle windows-arm64 \
203 codewhale-windows-arm64.exe codew-windows-arm64.exe zip ""
204 bundle windows-arm64 \
205 codewhale-windows-arm64.exe codew-windows-arm64.exe zip portable
206
207 sort -o "${manifest}" "${manifest}"
208 echo "Bundle checksum manifest:"
209 cat "${manifest}"
210
210 lines BASH