File size: 7,342 Bytes
c0af099 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | #!/usr/bin/env node
/**
* Download the uv binary for the current platform into resources/bin/
* so that electron-builder can bundle it as an extraResource.
*
* uv provides uvx, which the Electron desktop app uses to run the
* agent-server and automation backend Python packages.
*
* Usage:
* node scripts/download-uv.mjs # uses latest GitHub release
* UV_VERSION=0.7.0 node scripts/download-uv.mjs
*
* Output (per platform):
* resources/bin/uv + resources/bin/uvx (macOS / Linux)
* resources/bin/uv.exe + resources/bin/uvx.exe (Windows)
*/
import {
chmodSync,
copyFileSync,
createWriteStream,
existsSync,
mkdirSync,
rmSync,
} from "node:fs";
import { get } from "node:https";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { execFileSync } from "node:child_process";
const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, "..");
const outDir = join(projectRoot, "resources", "bin");
// ββ Platform detection βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const PLATFORM = process.platform; // 'darwin' | 'linux' | 'win32'
const ARCH = process.arch; // 'x64' | 'arm64'
function getPlatformSpec() {
if (PLATFORM === "darwin") {
const uvArch = ARCH === "arm64" ? "aarch64" : "x86_64";
return {
target: `uv-${uvArch}-apple-darwin`,
ext: "tar.gz",
binaries: ["uv", "uvx"],
};
}
if (PLATFORM === "linux") {
// Only x64 is officially supported by uv for desktop builds
return {
target: "uv-x86_64-unknown-linux-gnu",
ext: "tar.gz",
binaries: ["uv", "uvx"],
};
}
if (PLATFORM === "win32") {
return {
target: "uv-x86_64-pc-windows-msvc",
ext: "zip",
binaries: ["uv.exe", "uvx.exe"],
};
}
throw new Error(`Unsupported platform for uv download: ${PLATFORM}`);
}
// ββ Version resolution ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function resolveVersion() {
if (process.env.UV_VERSION) {
return process.env.UV_VERSION.replace(/^v/, "");
}
console.log("[download-uv] Fetching latest uv version from GitHub API...");
const headers = { "User-Agent": "agent-canvas-build" };
// Unauthenticated api.github.com calls are rate-limited per IP (60/hour) β
// shared CI runner IPs exhaust that fast. CI passes GITHUB_TOKEN.
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
const data = await fetchJson(
"https://api.github.com/repos/astral-sh/uv/releases/latest",
headers
);
const version = data.tag_name?.replace(/^v/, "");
if (!version) throw new Error("Could not parse uv version from GitHub API");
return version;
}
// ββ HTTP helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function fetchJson(url, headers = {}) {
return new Promise((resolve, reject) => {
get(url, { headers }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return resolve(fetchJson(res.headers.location, headers));
}
if (res.statusCode !== 200) {
return reject(new Error(`GET ${url} β HTTP ${res.statusCode}`));
}
let body = "";
res.on("data", (chunk) => (body += chunk));
res.on("end", () => resolve(JSON.parse(body)));
res.on("error", reject);
}).on("error", reject);
});
}
function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
function doGet(u) {
get(u, { headers: { "User-Agent": "agent-canvas-build" } }, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
return doGet(res.headers.location);
}
if (res.statusCode !== 200) {
file.destroy();
return reject(new Error(`GET ${u} β HTTP ${res.statusCode}`));
}
res.pipe(file);
file.on("finish", () => file.close(resolve));
file.on("error", reject);
res.on("error", reject);
}).on("error", (err) => {
file.destroy();
reject(err);
});
}
doGet(url);
});
}
// ββ Extraction ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function extract(archivePath, targetDir, ext) {
// Both tar.gz and zip are handled by the system 'tar' command:
// macOS/Linux: GNU/BSD tar natively supports .tar.gz
// Windows 10+: built-in bsdtar supports both .tar.gz and .zip
// The tar.gz archives wrap uv/uvx in a top-level `uv-<target>/` directory,
// which --strip-components=1 removes. uv's Windows .zip is flat (uv.exe /
// uvx.exe at the archive root) β stripping there would skip every entry
// and extract nothing.
const args = ["-xf", archivePath, "-C", targetDir];
if (ext === "tar.gz") {
args.push("--strip-components=1");
}
execFileSync("tar", args, { stdio: "inherit" });
}
// ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function main() {
const spec = getPlatformSpec();
const version = await resolveVersion();
console.log(`[download-uv] Downloading uv v${version} for ${PLATFORM}/${ARCH}`);
const filename = `${spec.target}.${spec.ext}`;
const url = `https://github.com/astral-sh/uv/releases/download/${version}/${filename}`;
const tmpFile = join(tmpdir(), `uv-download-${Date.now()}.${spec.ext}`);
const extractDir = join(tmpdir(), `uv-extract-${Date.now()}`);
try {
mkdirSync(outDir, { recursive: true });
mkdirSync(extractDir, { recursive: true });
console.log(`[download-uv] URL: ${url}`);
await downloadFile(url, tmpFile);
console.log(`[download-uv] Extracting to ${outDir}...`);
extract(tmpFile, extractDir, spec.ext);
// Copy the required binaries to outDir
for (const bin of spec.binaries) {
const src = join(extractDir, bin);
const dest = join(outDir, bin);
if (!existsSync(src)) {
throw new Error(`Expected binary not found after extraction: ${src}`);
}
// copyFileSync works across filesystems (unlike renameSync with EXDEV)
copyFileSync(src, dest);
if (process.platform !== "win32") {
chmodSync(dest, 0o755);
}
console.log(`[download-uv] β ${dest}`);
}
console.log("[download-uv] Done. Binaries are ready for bundling.");
} finally {
// Clean up temp files (best-effort)
try { rmSync(tmpFile, { force: true }); } catch {}
try { rmSync(extractDir, { recursive: true, force: true }); } catch {}
}
}
main().catch((err) => {
console.error("[download-uv] Error:", err.message);
process.exit(1);
});
|