1662 lines
62 KiB
JavaScript
1662 lines
62 KiB
JavaScript
#!/usr/bin/env bun
|
|||
|
|
// @bun
|
||
|
|
//
|
||
|
|
// xmac — download and extract macOS SDKs from Apple's public software-update
|
||
|
|
// CDN for cross-compilation, without redistributing Apple's SDK. Like xwin,
|
||
|
|
// but for macOS. Used by scripts/build/macos-sdk.ts to obtain the macOS SDK
|
||
|
|
// when cross-compiling darwin targets from a Linux host.
|
||
|
|
//
|
||
|
|
// VENDORED, GENERATED FILE — do not edit by hand.
|
||
|
|
// source: https://github.com/jarred-sumner/xmac
|
||
|
|
// commit: f68d2181f1e0fe52ac4c141efe076e262962e855
|
||
|
|
// regenerate: bun build --minify-syntax --target=bun xmac.ts --outfile=<bun>/scripts/build/xmac.mjs
|
||
|
|
// (then re-add this header)
|
||
|
|
//
|
||
|
|
// The tool only talks to swscan.apple.com / swdist.apple.com / swcdn.apple.com
|
||
|
|
// and requires `xz` on PATH. Run `bun scripts/build/xmac.mjs --help` for usage.
|
||
|
|
//
|
||
|
|
|
||
|
|
// src/main.ts
|
||
|
|
import * as fs7 from "fs";
|
||
|
|
import * as path7 from "path";
|
||
|
|
|
||
|
|
// src/util.ts
|
||
|
|
var VERSION = "0.1.0", DEFAULT_SUCATALOG = "https://swscan.apple.com/content/catalogs/others/index-26-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz", USER_AGENT = "xmac/0.1.0 (Software%20Update; like swupd)";
|
||
|
|
|
||
|
|
class XmacError extends Error {
|
||
|
|
}
|
||
|
|
function die(msg) {
|
||
|
|
console.error(`error: ${msg}`), process.exit(1);
|
||
|
|
}
|
||
|
|
var quiet = !1;
|
||
|
|
function setQuiet(q) {
|
||
|
|
quiet = q;
|
||
|
|
}
|
||
|
|
function isQuiet() {
|
||
|
|
return quiet;
|
||
|
|
}
|
||
|
|
function log(msg) {
|
||
|
|
if (!quiet)
|
||
|
|
console.error(msg);
|
||
|
|
}
|
||
|
|
function humanSize(n) {
|
||
|
|
if (n < 1024)
|
||
|
|
return `${n} B`;
|
||
|
|
let units = ["KiB", "MiB", "GiB", "TiB"], v = n, i = -1;
|
||
|
|
do
|
||
|
|
v /= 1024, i++;
|
||
|
|
while (v >= 1024 && i < units.length - 1);
|
||
|
|
return `${v.toFixed(v >= 100 ? 0 : 1)} ${units[i]}`;
|
||
|
|
}
|
||
|
|
function thousands(n) {
|
||
|
|
return n.toLocaleString("en-US");
|
||
|
|
}
|
||
|
|
function asBuffer(u8) {
|
||
|
|
return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength);
|
||
|
|
}
|
||
|
|
function sha1Hex(buf) {
|
||
|
|
let h = new Bun.CryptoHasher("sha1");
|
||
|
|
return h.update(buf), h.digest("hex");
|
||
|
|
}
|
||
|
|
function compareVersions(a, b) {
|
||
|
|
let pa = a.split(".").map((x) => parseInt(x, 10) || 0), pb = b.split(".").map((x) => parseInt(x, 10) || 0);
|
||
|
|
for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
|
||
|
|
let d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||
|
|
if (d !== 0)
|
||
|
|
return d;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
async function mapLimit(items, limit, fn) {
|
||
|
|
let results = Array.from({ length: items.length }), next = 0, workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||
|
|
while (!0) {
|
||
|
|
let i = next++;
|
||
|
|
if (i >= items.length)
|
||
|
|
return;
|
||
|
|
results[i] = await fn(items[i], i);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return await Promise.all(workers), results;
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/commands.ts
|
||
|
|
import * as fs6 from "fs";
|
||
|
|
import * as path6 from "path";
|
||
|
|
|
||
|
|
// src/ui.ts
|
||
|
|
var interactive = Boolean(process.stderr.isTTY), colorEnabled = process.env.FORCE_COLOR !== void 0 ? process.env.FORCE_COLOR !== "0" : interactive && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb", wrap = (open, close) => (s) => colorEnabled ? `\x1B[${open}m${s}\x1B[${close}m` : s, c = {
|
||
|
|
bold: wrap("1", "22"),
|
||
|
|
dim: wrap("2", "22"),
|
||
|
|
red: wrap("31", "39"),
|
||
|
|
green: wrap("32", "39"),
|
||
|
|
yellow: wrap("33", "39"),
|
||
|
|
cyan: wrap("36", "39"),
|
||
|
|
magenta: wrap("35", "39")
|
||
|
|
};
|
||
|
|
function eprintln(line) {
|
||
|
|
process.stderr.write(line + `
|
||
|
|
`);
|
||
|
|
}
|
||
|
|
function status(msg) {
|
||
|
|
if (isQuiet())
|
||
|
|
return;
|
||
|
|
eprintln(interactive ? `${c.cyan("\u2192")} ${msg}` : msg);
|
||
|
|
}
|
||
|
|
function ok(msg) {
|
||
|
|
if (isQuiet())
|
||
|
|
return;
|
||
|
|
eprintln(interactive ? `${c.green("\u2713")} ${msg}` : msg);
|
||
|
|
}
|
||
|
|
function warn(msg) {
|
||
|
|
eprintln(interactive ? `${c.yellow("!")} ${msg}` : `warning: ${msg}`);
|
||
|
|
}
|
||
|
|
function note(msg) {
|
||
|
|
if (isQuiet())
|
||
|
|
return;
|
||
|
|
eprintln(interactive ? ` ${c.dim(msg)}` : ` ${msg}`);
|
||
|
|
}
|
||
|
|
function result(pairs) {
|
||
|
|
if (interactive) {
|
||
|
|
let w = Math.max(...pairs.map(([k]) => k.length));
|
||
|
|
for (let [k, v] of pairs)
|
||
|
|
console.log(` ${c.dim(k.padEnd(w))} ${v}`);
|
||
|
|
} else
|
||
|
|
for (let [k, v] of pairs)
|
||
|
|
console.log(`${k}: ${v}`);
|
||
|
|
}
|
||
|
|
function table(rows) {
|
||
|
|
if (rows.length === 0)
|
||
|
|
return;
|
||
|
|
let widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length))), fmt = (row, decorate) => " " + row.map((cell, i) => decorate(i === row.length - 1 ? cell : cell.padEnd(widths[i]))).join(" ");
|
||
|
|
rows.forEach((row, idx) => {
|
||
|
|
if (idx === 0)
|
||
|
|
console.log(fmt(row, (s) => c.bold(s)));
|
||
|
|
else
|
||
|
|
console.log(fmt(row, (s) => s));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
class Progress {
|
||
|
|
label;
|
||
|
|
total;
|
||
|
|
last = 0;
|
||
|
|
constructor(label, total) {
|
||
|
|
this.label = label;
|
||
|
|
this.total = total;
|
||
|
|
}
|
||
|
|
update(done) {
|
||
|
|
if (isQuiet() || !interactive)
|
||
|
|
return;
|
||
|
|
let now = Date.now();
|
||
|
|
if (now - this.last < 100 && done < this.total)
|
||
|
|
return;
|
||
|
|
this.last = now;
|
||
|
|
let pct = this.total > 0 ? Math.min(100, Math.floor(done / this.total * 100)) : 0, cols = process.stderr.columns || 80, suffix = ` ${String(pct).padStart(3)}% ${humanSize(done)} / ${humanSize(this.total)}`, label = this.label, width = Math.min(28, cols - 1 - 2 - label.length - 1 - suffix.length);
|
||
|
|
if (width < 10)
|
||
|
|
suffix = ` ${String(pct).padStart(3)}%`, width = Math.min(28, cols - 1 - 2 - label.length - 1 - suffix.length);
|
||
|
|
if (width < 10) {
|
||
|
|
let room = Math.max(4, cols - 1 - 2 - 1 - suffix.length - 10);
|
||
|
|
label = label.length > room ? label.slice(0, room - 1) + "\u2026" : label, width = Math.min(28, cols - 1 - 2 - label.length - 1 - suffix.length);
|
||
|
|
}
|
||
|
|
if (width < 4) {
|
||
|
|
process.stderr.write(`\r ${String(pct).padStart(3)}%\x1B[K`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let filled = Math.min(width, Math.floor(width * pct / 100)), bar = c.cyan("\u2588".repeat(filled)) + c.dim("\u2591".repeat(width - filled));
|
||
|
|
process.stderr.write(`\r ${label} ${bar}${suffix}\x1B[K`);
|
||
|
|
}
|
||
|
|
finish() {
|
||
|
|
if (isQuiet() || !interactive)
|
||
|
|
return;
|
||
|
|
process.stderr.write("\r\x1B[K");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/xml.ts
|
||
|
|
function decodeEntities(s) {
|
||
|
|
if (!s.includes("&"))
|
||
|
|
return s;
|
||
|
|
return s.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, ent) => {
|
||
|
|
if (ent[0] === "#") {
|
||
|
|
let code = ent[1] === "x" || ent[1] === "X" ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
|
||
|
|
return Number.isFinite(code) ? String.fromCodePoint(code) : m;
|
||
|
|
}
|
||
|
|
switch (ent) {
|
||
|
|
case "lt":
|
||
|
|
return "<";
|
||
|
|
case "gt":
|
||
|
|
return ">";
|
||
|
|
case "amp":
|
||
|
|
return "&";
|
||
|
|
case "quot":
|
||
|
|
return '"';
|
||
|
|
case "apos":
|
||
|
|
return "'";
|
||
|
|
default:
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
function parseXml(input) {
|
||
|
|
let i = 0, n = input.length, root = { tag: "#root", attrs: {}, children: [], text: "" }, stack = [root], top = () => stack[stack.length - 1];
|
||
|
|
while (i < n) {
|
||
|
|
let lt = input.indexOf("<", i);
|
||
|
|
if (lt === -1) {
|
||
|
|
top().text += decodeEntities(input.slice(i));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (lt > i)
|
||
|
|
top().text += decodeEntities(input.slice(i, lt));
|
||
|
|
if (i = lt, input.startsWith("<!--", i)) {
|
||
|
|
let end2 = input.indexOf("-->", i + 4);
|
||
|
|
if (end2 === -1)
|
||
|
|
throw new XmacError("xml: unterminated comment");
|
||
|
|
i = end2 + 3;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (input.startsWith("<![CDATA[", i)) {
|
||
|
|
let end2 = input.indexOf("]]>", i + 9);
|
||
|
|
if (end2 === -1)
|
||
|
|
throw new XmacError("xml: unterminated CDATA");
|
||
|
|
top().text += input.slice(i + 9, end2), i = end2 + 3;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (input.startsWith("<!", i) || input.startsWith("<?", i)) {
|
||
|
|
let end2 = input.indexOf(">", i);
|
||
|
|
if (end2 === -1)
|
||
|
|
throw new XmacError("xml: unterminated declaration");
|
||
|
|
i = end2 + 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (input.startsWith("</", i)) {
|
||
|
|
let end2 = input.indexOf(">", i);
|
||
|
|
if (end2 === -1)
|
||
|
|
throw new XmacError("xml: unterminated close tag");
|
||
|
|
let name = input.slice(i + 2, end2).trim();
|
||
|
|
if (stack.length < 2 || top().tag !== name)
|
||
|
|
throw new XmacError(`xml: mismatched close tag </${name}> (open: <${top().tag}>)`);
|
||
|
|
stack.pop(), i = end2 + 1;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let end = input.indexOf(">", i);
|
||
|
|
if (end === -1)
|
||
|
|
throw new XmacError("xml: unterminated open tag");
|
||
|
|
let tagBody = input.slice(i + 1, end);
|
||
|
|
i = end + 1;
|
||
|
|
let selfClose = !1;
|
||
|
|
if (tagBody.endsWith("/"))
|
||
|
|
selfClose = !0, tagBody = tagBody.slice(0, -1);
|
||
|
|
let m = /^([^\s]+)\s*/.exec(tagBody);
|
||
|
|
if (!m)
|
||
|
|
throw new XmacError("xml: empty tag");
|
||
|
|
let node = { tag: m[1], attrs: {}, children: [], text: "" }, rest = tagBody.slice(m[0].length), attrRe = /([^\s=]+)\s*=\s*("([^"]*)"|'([^']*)')\s*/g, am;
|
||
|
|
while ((am = attrRe.exec(rest)) !== null)
|
||
|
|
node.attrs[am[1]] = decodeEntities(am[3] ?? am[4] ?? "");
|
||
|
|
if (top().children.push(node), !selfClose)
|
||
|
|
stack.push(node);
|
||
|
|
}
|
||
|
|
if (stack.length !== 1)
|
||
|
|
throw new XmacError(`xml: unclosed element <${top().tag}>`);
|
||
|
|
return root;
|
||
|
|
}
|
||
|
|
function firstChild(node, tag) {
|
||
|
|
return node.children.find((co) => co.tag === tag);
|
||
|
|
}
|
||
|
|
function childText(node, tag) {
|
||
|
|
let co = firstChild(node, tag);
|
||
|
|
return co ? co.text.trim() : void 0;
|
||
|
|
}
|
||
|
|
function plistToJs(node) {
|
||
|
|
switch (node.tag) {
|
||
|
|
case "dict": {
|
||
|
|
let out = {}, kids = node.children;
|
||
|
|
for (let i = 0;i < kids.length; i++) {
|
||
|
|
if (kids[i].tag !== "key")
|
||
|
|
continue;
|
||
|
|
let key = kids[i].text, val = kids[i + 1];
|
||
|
|
if (!val)
|
||
|
|
break;
|
||
|
|
out[key] = plistToJs(val), i++;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
case "array":
|
||
|
|
return node.children.map(plistToJs);
|
||
|
|
case "string":
|
||
|
|
return node.text;
|
||
|
|
case "integer":
|
||
|
|
return parseInt(node.text.trim(), 10);
|
||
|
|
case "real":
|
||
|
|
return parseFloat(node.text.trim());
|
||
|
|
case "date":
|
||
|
|
return new Date(node.text.trim());
|
||
|
|
case "true":
|
||
|
|
return !0;
|
||
|
|
case "false":
|
||
|
|
return !1;
|
||
|
|
case "data":
|
||
|
|
return Uint8Array.from(atob(node.text.replace(/\s+/g, "")), (ch) => ch.charCodeAt(0));
|
||
|
|
default:
|
||
|
|
throw new XmacError(`plist: unexpected element <${node.tag}>`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function parsePlist(xml) {
|
||
|
|
let root = parseXml(xml), plist = firstChild(root, "plist");
|
||
|
|
if (!plist || plist.children.length === 0)
|
||
|
|
throw new XmacError("plist: missing <plist> root");
|
||
|
|
return plistToJs(plist.children[0]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/net.ts
|
||
|
|
import * as fs from "fs";
|
||
|
|
import * as path from "path";
|
||
|
|
async function httpGet(url, opts = {}) {
|
||
|
|
let retries = opts.retries ?? 3, lastErr;
|
||
|
|
for (let attempt = 0;attempt <= retries; attempt++) {
|
||
|
|
if (attempt > 0) {
|
||
|
|
let delay = Math.min(1000 * 2 ** (attempt - 1), 8000);
|
||
|
|
await Bun.sleep(delay), log(` retrying (${attempt}/${retries}) ${url}`);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
let headers = { "User-Agent": USER_AGENT };
|
||
|
|
if (opts.range)
|
||
|
|
headers.Range = `bytes=${opts.range[0]}-${opts.range[1]}`;
|
||
|
|
let res = await fetch(url, { headers, redirect: "follow" });
|
||
|
|
if (res.status === 200 || res.status === 206)
|
||
|
|
return res;
|
||
|
|
if (res.status >= 400 && res.status < 500 && res.status !== 429)
|
||
|
|
throw new XmacError(`GET ${url}: HTTP ${res.status}`);
|
||
|
|
lastErr = new XmacError(`GET ${url}: HTTP ${res.status}`);
|
||
|
|
} catch (e) {
|
||
|
|
if (e instanceof XmacError)
|
||
|
|
throw e;
|
||
|
|
lastErr = e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw lastErr instanceof Error ? lastErr : new XmacError(`GET ${url}: ${lastErr}`);
|
||
|
|
}
|
||
|
|
async function httpGetBytes(url, range) {
|
||
|
|
let res = await httpGet(url, { range });
|
||
|
|
return new Uint8Array(await res.arrayBuffer());
|
||
|
|
}
|
||
|
|
async function downloadTo(url, dest, expectedSize, expectedSha1, label) {
|
||
|
|
let name = label ?? path.basename(dest);
|
||
|
|
if (fs.existsSync(dest)) {
|
||
|
|
if (fs.statSync(dest).size === expectedSize) {
|
||
|
|
if (!expectedSha1) {
|
||
|
|
note(`${name}: cached`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let h = new Bun.CryptoHasher("sha1"), fd = fs.openSync(dest, "r");
|
||
|
|
try {
|
||
|
|
let buf = Buffer.alloc(4194304), r;
|
||
|
|
while ((r = fs.readSync(fd, buf, 0, buf.length, -1)) > 0)
|
||
|
|
h.update(buf.subarray(0, r));
|
||
|
|
} finally {
|
||
|
|
fs.closeSync(fd);
|
||
|
|
}
|
||
|
|
if (h.digest("hex") === expectedSha1.toLowerCase()) {
|
||
|
|
note(`${name}: cached (checksum ok)`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
note(`${name}: cached file failed checksum, re-downloading`);
|
||
|
|
}
|
||
|
|
fs.rmSync(dest, { force: !0 });
|
||
|
|
}
|
||
|
|
fs.mkdirSync(path.dirname(dest), { recursive: !0 });
|
||
|
|
let tmp = `${dest}.part`, res = await httpGet(url), total = Number(res.headers.get("content-length") ?? expectedSize) || expectedSize, prog = new Progress(name, total), hasher = new Bun.CryptoHasher("sha1"), sink = fs.createWriteStream(tmp), done = 0;
|
||
|
|
try {
|
||
|
|
if (!res.body)
|
||
|
|
throw new XmacError(`GET ${url}: empty body`);
|
||
|
|
for await (let chunk of res.body) {
|
||
|
|
let u8 = chunk;
|
||
|
|
if (hasher.update(u8), done += u8.byteLength, prog.update(done), !sink.write(u8))
|
||
|
|
await new Promise((r) => sink.once("drain", () => r()));
|
||
|
|
}
|
||
|
|
if (await new Promise((resolve, reject) => sink.end((err) => err ? reject(err) : resolve())), expectedSize && done !== expectedSize)
|
||
|
|
throw new XmacError(`${url}: size mismatch (got ${done}, expected ${expectedSize})`);
|
||
|
|
let got = hasher.digest("hex");
|
||
|
|
if (expectedSha1 && got !== expectedSha1.toLowerCase())
|
||
|
|
throw new XmacError(`${url}: SHA-1 mismatch (got ${got}, expected ${expectedSha1})`);
|
||
|
|
} catch (e) {
|
||
|
|
throw sink.destroy(), fs.rmSync(tmp, { force: !0 }), e;
|
||
|
|
}
|
||
|
|
fs.renameSync(tmp, dest), prog.finish(), ok(`downloaded ${name} (${humanSize(done)})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/catalog.ts
|
||
|
|
import * as fs3 from "fs";
|
||
|
|
import * as path3 from "path";
|
||
|
|
import * as zlib2 from "zlib";
|
||
|
|
|
||
|
|
// src/xar.ts
|
||
|
|
import * as fs2 from "fs";
|
||
|
|
import * as path2 from "path";
|
||
|
|
import * as zlib from "zlib";
|
||
|
|
|
||
|
|
// src/exec.ts
|
||
|
|
var toolCache = /* @__PURE__ */ new Map;
|
||
|
|
function requireTool(name) {
|
||
|
|
let p = toolCache.get(name);
|
||
|
|
if (p)
|
||
|
|
return p;
|
||
|
|
if (p = Bun.which(name) ?? "", !p)
|
||
|
|
throw new XmacError(`required tool '${name}' not found on PATH. Install it (e.g. apt-get install ${name === "xz" ? "xz-utils" : name}) and retry.`);
|
||
|
|
return toolCache.set(name, p), p;
|
||
|
|
}
|
||
|
|
async function decompressWith(tool, args, input, allowFailure = !1) {
|
||
|
|
let proc = Bun.spawn([requireTool(tool), ...args], {
|
||
|
|
stdin: "pipe",
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: allowFailure ? "ignore" : "pipe"
|
||
|
|
}), writer = (async () => {
|
||
|
|
try {
|
||
|
|
proc.stdin.write(input), await proc.stdin.end();
|
||
|
|
} catch {}
|
||
|
|
})(), out = new Uint8Array(await new Response(proc.stdout).arrayBuffer());
|
||
|
|
await writer;
|
||
|
|
let code = await proc.exited;
|
||
|
|
if (code !== 0 && !allowFailure) {
|
||
|
|
let err = proc.stderr ? await new Response(proc.stderr).text() : `exit ${code}`;
|
||
|
|
throw new XmacError(`${tool} failed: ${err.trim() || `exit ${code}`}`);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/xar.ts
|
||
|
|
var XAR_MAGIC = 2019652129;
|
||
|
|
function parseXarHeader(buf) {
|
||
|
|
if (buf.length < 28)
|
||
|
|
throw new XmacError("xar: file too small");
|
||
|
|
let b = asBuffer(buf);
|
||
|
|
if (b.readUInt32BE(0) !== XAR_MAGIC)
|
||
|
|
throw new XmacError("xar: bad magic \u2014 this does not look like a flat .pkg/.xar file");
|
||
|
|
let headerSize = b.readUInt16BE(4), version = b.readUInt16BE(6);
|
||
|
|
if (version !== 1)
|
||
|
|
throw new XmacError(`xar: unsupported version ${version}`);
|
||
|
|
let tocCompressedLength = Number(b.readBigUInt64BE(8)), tocUncompressedLength = Number(b.readBigUInt64BE(16));
|
||
|
|
return {
|
||
|
|
headerSize,
|
||
|
|
tocCompressedLength,
|
||
|
|
tocUncompressedLength,
|
||
|
|
heapStart: headerSize + tocCompressedLength
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function parseXarToc(tocXml) {
|
||
|
|
let root = parseXml(tocXml), xar = firstChild(root, "xar"), toc = xar && firstChild(xar, "toc");
|
||
|
|
if (!toc)
|
||
|
|
throw new XmacError("xar: missing <toc>");
|
||
|
|
let entries = [], walk = (node, prefix) => {
|
||
|
|
for (let f of node.children) {
|
||
|
|
if (f.tag !== "file")
|
||
|
|
continue;
|
||
|
|
let name = childText(f, "name") ?? "", full = prefix ? `${prefix}/${name}` : name, type = childText(f, "type") ?? "file", data = firstChild(f, "data"), offset = 0, size = 0, length = 0, encoding = "application/octet-stream", archivedChecksum;
|
||
|
|
if (data) {
|
||
|
|
offset = parseInt(childText(data, "offset") ?? "0", 10), size = parseInt(childText(data, "size") ?? "0", 10), length = parseInt(childText(data, "length") ?? "0", 10);
|
||
|
|
let enc = firstChild(data, "encoding");
|
||
|
|
if (enc?.attrs.style)
|
||
|
|
encoding = enc.attrs.style;
|
||
|
|
let ac = firstChild(data, "archived-checksum");
|
||
|
|
if (ac)
|
||
|
|
archivedChecksum = {
|
||
|
|
style: ac.attrs.style ?? "",
|
||
|
|
value: ac.text.trim()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
entries.push({
|
||
|
|
name: full,
|
||
|
|
type,
|
||
|
|
offset,
|
||
|
|
size,
|
||
|
|
length,
|
||
|
|
encoding,
|
||
|
|
archivedChecksum
|
||
|
|
}), walk(f, full);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
return walk(toc, ""), entries;
|
||
|
|
}
|
||
|
|
async function xarDecode(data, encoding) {
|
||
|
|
switch (encoding) {
|
||
|
|
case "application/octet-stream":
|
||
|
|
return data;
|
||
|
|
case "application/x-gzip":
|
||
|
|
case "application/zlib":
|
||
|
|
case "application/x-zlib":
|
||
|
|
return zlib.inflateSync(data);
|
||
|
|
case "application/x-bzip2":
|
||
|
|
case "application/bzip2":
|
||
|
|
return decompressWith("bzip2", ["-dc"], data);
|
||
|
|
case "application/x-lzma":
|
||
|
|
case "application/x-xz":
|
||
|
|
return decompressWith("xz", ["-dc", "-T0"], data);
|
||
|
|
default:
|
||
|
|
throw new XmacError(`xar: unsupported encoding '${encoding}'`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class XarFile {
|
||
|
|
fd;
|
||
|
|
header;
|
||
|
|
entries;
|
||
|
|
filePath;
|
||
|
|
constructor(fd, header, entries, filePath) {
|
||
|
|
this.fd = fd;
|
||
|
|
this.header = header;
|
||
|
|
this.entries = entries;
|
||
|
|
this.filePath = filePath;
|
||
|
|
}
|
||
|
|
static open(filePath) {
|
||
|
|
let fd = fs2.openSync(filePath, "r");
|
||
|
|
try {
|
||
|
|
let head = Buffer.alloc(28);
|
||
|
|
fs2.readSync(fd, head, 0, 28, 0);
|
||
|
|
let header = parseXarHeader(head), ctoc = Buffer.alloc(header.tocCompressedLength);
|
||
|
|
fs2.readSync(fd, ctoc, 0, ctoc.length, header.headerSize);
|
||
|
|
let toc = zlib.inflateSync(ctoc), entries = parseXarToc((/* @__PURE__ */ new TextDecoder()).decode(toc));
|
||
|
|
return new XarFile(fd, header, entries, filePath);
|
||
|
|
} catch (e) {
|
||
|
|
throw fs2.closeSync(fd), e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
close() {
|
||
|
|
fs2.closeSync(this.fd);
|
||
|
|
}
|
||
|
|
find(name) {
|
||
|
|
return this.entries.find((e) => e.name === name);
|
||
|
|
}
|
||
|
|
async readEntry(entry) {
|
||
|
|
let raw = Buffer.alloc(entry.size);
|
||
|
|
if (fs2.readSync(this.fd, raw, 0, entry.size, this.header.heapStart + entry.offset), entry.archivedChecksum?.style === "sha1") {
|
||
|
|
if (sha1Hex(raw) !== entry.archivedChecksum.value.toLowerCase())
|
||
|
|
throw new XmacError(`xar: checksum mismatch for '${entry.name}' in ${path2.basename(this.filePath)}`);
|
||
|
|
}
|
||
|
|
return xarDecode(raw, entry.encoding);
|
||
|
|
}
|
||
|
|
async* streamRaw(entry, sliceSize = 4194304) {
|
||
|
|
if (entry.encoding !== "application/octet-stream")
|
||
|
|
throw new XmacError(`xar: cannot stream '${entry.name}' with encoding ${entry.encoding}`);
|
||
|
|
let algo = entry.archivedChecksum?.style, hasher = algo === "sha1" || algo === "sha256" || algo === "sha512" || algo === "md5" ? new Bun.CryptoHasher(algo) : void 0, pos = 0;
|
||
|
|
while (pos < entry.size) {
|
||
|
|
let n = Math.min(sliceSize, entry.size - pos), buf = Buffer.alloc(n);
|
||
|
|
if (fs2.readSync(this.fd, buf, 0, n, this.header.heapStart + entry.offset + pos) !== n)
|
||
|
|
throw new XmacError(`xar: short read in '${entry.name}'`);
|
||
|
|
pos += n, hasher?.update(buf), yield buf;
|
||
|
|
}
|
||
|
|
if (hasher) {
|
||
|
|
if (hasher.digest("hex") !== entry.archivedChecksum.value.toLowerCase())
|
||
|
|
throw new XmacError(`xar: ${algo} checksum mismatch for '${entry.name}' in ${path2.basename(this.filePath)} \u2014 the download is corrupt; delete it from the cache and retry`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/catalog.ts
|
||
|
|
function displayName(r) {
|
||
|
|
return r.title.endsWith(r.version) ? r.title : `${r.title} ${r.version}`;
|
||
|
|
}
|
||
|
|
function isSdkPackage(url) {
|
||
|
|
let f = url.split("/").pop() ?? "";
|
||
|
|
return /^CLTools_.*SDK.*\.pkg$/i.test(f) && !/DevSDK_Remove/i.test(f);
|
||
|
|
}
|
||
|
|
async function fetchCatalog(catalogUrl) {
|
||
|
|
let data = await httpGetBytes(catalogUrl);
|
||
|
|
if (data[0] === 31 && data[1] === 139)
|
||
|
|
data = zlib2.gunzipSync(data);
|
||
|
|
return parsePlist((/* @__PURE__ */ new TextDecoder()).decode(data));
|
||
|
|
}
|
||
|
|
function extractCltProducts(catalog) {
|
||
|
|
let products = catalog.Products;
|
||
|
|
if (!products)
|
||
|
|
throw new XmacError("catalog: missing Products dict");
|
||
|
|
let out = [];
|
||
|
|
for (let [productId, p] of Object.entries(products)) {
|
||
|
|
let prod = p, packages = (prod.Packages ?? []).map((x) => {
|
||
|
|
let d = x;
|
||
|
|
return {
|
||
|
|
url: String(d.URL ?? ""),
|
||
|
|
size: Number(d.Size ?? 0),
|
||
|
|
digest: d.Digest ? String(d.Digest) : void 0,
|
||
|
|
metadataUrl: d.MetadataURL ? String(d.MetadataURL) : void 0
|
||
|
|
};
|
||
|
|
});
|
||
|
|
if (!packages.some((x) => /\/CLTools_/.test(x.url)))
|
||
|
|
continue;
|
||
|
|
if (!packages.some((x) => isSdkPackage(x.url)))
|
||
|
|
continue;
|
||
|
|
let dists = prod.Distributions, distributionUrl = dists ? String(dists.English ?? dists.en ?? Object.values(dists)[0] ?? "") : void 0, postDate = prod.PostDate;
|
||
|
|
out.push({
|
||
|
|
productId,
|
||
|
|
postDate: postDate instanceof Date ? postDate.toISOString().slice(0, 10) : String(postDate ?? ""),
|
||
|
|
distributionUrl: distributionUrl || void 0,
|
||
|
|
packages
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
async function fetchDistInfo(url) {
|
||
|
|
let text = (/* @__PURE__ */ new TextDecoder()).decode(await httpGetBytes(url)), grab = (key) => {
|
||
|
|
let m = new RegExp(`"${key}"\\s*=\\s*"([^"]*)"`).exec(text);
|
||
|
|
return m ? m[1] : void 0;
|
||
|
|
}, title = grab("SU_TITLE"), version = grab("SU_VERS");
|
||
|
|
if (!title) {
|
||
|
|
let m = /<title>([^<]*)<\/title>/.exec(text);
|
||
|
|
if (m && m[1] !== "SU_TITLE")
|
||
|
|
title = m[1];
|
||
|
|
}
|
||
|
|
if (!version) {
|
||
|
|
let m = /<pkg-ref[^>]*\bversion="(\d+\.\d+)/.exec(text);
|
||
|
|
if (m)
|
||
|
|
version = m[1];
|
||
|
|
}
|
||
|
|
return { title: title ?? "Command Line Tools", version: version ?? "?" };
|
||
|
|
}
|
||
|
|
async function fetchDistribution(release) {
|
||
|
|
if (!release.distributionUrl)
|
||
|
|
throw new XmacError("no distribution document is available for this release");
|
||
|
|
return (/* @__PURE__ */ new TextDecoder()).decode(await httpGetBytes(release.distributionUrl));
|
||
|
|
}
|
||
|
|
async function peekSdkName(pkg) {
|
||
|
|
try {
|
||
|
|
let head = await httpGetBytes(pkg.url, [0, 65535]), hdr = parseXarHeader(head);
|
||
|
|
if (head.length < hdr.headerSize + hdr.tocCompressedLength)
|
||
|
|
head = await httpGetBytes(pkg.url, [0, hdr.headerSize + hdr.tocCompressedLength - 1]);
|
||
|
|
let toc = zlib2.inflateSync(head.subarray(hdr.headerSize, hdr.headerSize + hdr.tocCompressedLength)), payload = parseXarToc((/* @__PURE__ */ new TextDecoder()).decode(toc)).find((e) => e.name === "Payload");
|
||
|
|
if (!payload)
|
||
|
|
return;
|
||
|
|
let start = hdr.heapStart + payload.offset, want = Math.min(payload.size, 98304), data = await httpGetBytes(pkg.url, [start, start + want - 1]), cpio;
|
||
|
|
if ((/* @__PURE__ */ new TextDecoder()).decode(data.subarray(0, 4)) === "pbzx") {
|
||
|
|
let b = asBuffer(data), uncompressedSize = Number(b.readBigUInt64BE(12)), compressedSize = Number(b.readBigUInt64BE(20)), chunk = data.subarray(28, Math.min(28 + compressedSize, data.length));
|
||
|
|
cpio = compressedSize === uncompressedSize ? chunk : await decompressWith("xz", ["-dcq"], chunk, !0);
|
||
|
|
} else if (data[0] === 31 && data[1] === 139)
|
||
|
|
cpio = await decompressWith("gzip", ["-dcq"], data, !0);
|
||
|
|
else
|
||
|
|
return;
|
||
|
|
let text = asBuffer(cpio.subarray(0, 65536)).toString("latin1"), m = /SDKs\/([A-Za-z0-9_.]+\.sdk)/.exec(text);
|
||
|
|
return m ? m[1] : void 0;
|
||
|
|
} catch {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function getManifest(opts, forceRefresh = !1) {
|
||
|
|
let cachePath = path3.join(opts.cacheDir, "manifest.json");
|
||
|
|
if (!forceRefresh && fs3.existsSync(cachePath))
|
||
|
|
try {
|
||
|
|
let m = JSON.parse(fs3.readFileSync(cachePath, "utf8"));
|
||
|
|
if (m.catalogUrl === opts.catalog && Array.isArray(m.releases)) {
|
||
|
|
let age = Date.now() - new Date(m.fetchedAt).getTime();
|
||
|
|
if (opts.offline || age < 86400000)
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
} catch {}
|
||
|
|
if (opts.offline)
|
||
|
|
throw new XmacError(`--offline was passed but no cached manifest exists at ${cachePath}. Run \`xmac list\` online once first.`);
|
||
|
|
log("Fetching Apple software update catalog\u2026");
|
||
|
|
let catalog = await fetchCatalog(opts.catalog), products = extractCltProducts(catalog);
|
||
|
|
log(`Resolving ${products.length} Command Line Tools releases\u2026`);
|
||
|
|
let filtered = (await mapLimit(products, 8, async (p) => {
|
||
|
|
let sdkPkgs = p.packages.filter((x) => isSdkPackage(x.url)).map((x) => ({
|
||
|
|
fileName: x.url.split("/").pop() ?? "pkg",
|
||
|
|
url: x.url,
|
||
|
|
size: x.size,
|
||
|
|
digest: x.digest
|
||
|
|
})).filter((x) => x.size > 1048576), info = p.distributionUrl ? await fetchDistInfo(p.distributionUrl).catch(() => ({
|
||
|
|
title: "Command Line Tools",
|
||
|
|
version: "?"
|
||
|
|
})) : { title: "Command Line Tools", version: "?" };
|
||
|
|
return await Promise.all(sdkPkgs.map(async (s) => {
|
||
|
|
if (s.sdkName = await peekSdkName(s), s.sdkName) {
|
||
|
|
let m = /^MacOSX(\d+(?:\.\d+)*)\.sdk$/i.exec(s.sdkName);
|
||
|
|
if (m)
|
||
|
|
s.sdkVersion = m[1];
|
||
|
|
}
|
||
|
|
})), {
|
||
|
|
productId: p.productId,
|
||
|
|
postDate: p.postDate,
|
||
|
|
version: info.version,
|
||
|
|
title: info.title,
|
||
|
|
distributionUrl: p.distributionUrl,
|
||
|
|
sdkPackages: sdkPkgs
|
||
|
|
};
|
||
|
|
})).filter((r) => r.sdkPackages.length > 0);
|
||
|
|
filtered.sort((a, b) => {
|
||
|
|
let d = a.postDate.localeCompare(b.postDate);
|
||
|
|
return d !== 0 ? d : compareVersions(a.version, b.version);
|
||
|
|
}), filtered.reverse();
|
||
|
|
let manifest = {
|
||
|
|
catalogUrl: opts.catalog,
|
||
|
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
||
|
|
releases: filtered
|
||
|
|
};
|
||
|
|
return fs3.mkdirSync(opts.cacheDir, { recursive: !0 }), fs3.writeFileSync(cachePath, JSON.stringify(manifest, null, 2)), manifest;
|
||
|
|
}
|
||
|
|
function selectSdks(manifest, sdkSpec, releaseSpec) {
|
||
|
|
let releases = manifest.releases;
|
||
|
|
if (releases.length === 0)
|
||
|
|
throw new XmacError("no Command Line Tools releases found in the catalog");
|
||
|
|
if (releaseSpec) {
|
||
|
|
if (releases = releases.filter((r) => r.productId === releaseSpec || r.version === releaseSpec), releases.length === 0)
|
||
|
|
throw new XmacError(`no Command Line Tools release matches '${releaseSpec}'. Run \`xmac list\` to see what is available.`);
|
||
|
|
}
|
||
|
|
let spec = sdkSpec.trim();
|
||
|
|
if (spec.toLowerCase() === "all") {
|
||
|
|
let release = releases[0];
|
||
|
|
return { release, packages: release.sdkPackages };
|
||
|
|
}
|
||
|
|
let candidates = [];
|
||
|
|
for (let r of releases)
|
||
|
|
for (let p of r.sdkPackages)
|
||
|
|
candidates.push({ release: r, pkg: p });
|
||
|
|
if (spec.toLowerCase() === "latest") {
|
||
|
|
let best;
|
||
|
|
for (let cand of candidates) {
|
||
|
|
if (!best) {
|
||
|
|
best = cand;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let a = cand.pkg.sdkVersion ?? "0", b = best.pkg.sdkVersion ?? "0";
|
||
|
|
if (compareVersions(a, b) > 0)
|
||
|
|
best = cand;
|
||
|
|
}
|
||
|
|
if (!best)
|
||
|
|
throw new XmacError("no SDK packages found");
|
||
|
|
return { release: best.release, packages: [best.pkg] };
|
||
|
|
}
|
||
|
|
let vm = /^(?:macosx)?(\d+(?:\.\d+)*)(?:\.sdk)?$/i.exec(spec);
|
||
|
|
if (!vm)
|
||
|
|
throw new XmacError(`invalid --sdk '${spec}'. Use 'latest', 'all', a version like '14.5', or a name like 'MacOSX14.5.sdk'.`);
|
||
|
|
let wanted = vm[1], exact = candidates.filter((cand) => cand.pkg.sdkVersion === wanted), prefix = candidates.filter((cand) => cand.pkg.sdkVersion && (cand.pkg.sdkVersion === wanted || cand.pkg.sdkVersion.startsWith(wanted + "."))), pool = exact.length > 0 ? exact : prefix;
|
||
|
|
if (pool.length === 0) {
|
||
|
|
let known = [
|
||
|
|
...new Set(candidates.map((cand) => cand.pkg.sdkVersion).filter(Boolean))
|
||
|
|
].sort(compareVersions);
|
||
|
|
throw new XmacError(`no macOS ${wanted} SDK found in the catalog.
|
||
|
|
Available SDK versions: ${known.join(", ")}`);
|
||
|
|
}
|
||
|
|
return pool = [...pool].sort((a, b) => {
|
||
|
|
let d = compareVersions(b.pkg.sdkVersion ?? "0", a.pkg.sdkVersion ?? "0");
|
||
|
|
return d !== 0 ? d : b.release.postDate.localeCompare(a.release.postDate);
|
||
|
|
}), { release: pool[0].release, packages: [pool[0].pkg] };
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/extract.ts
|
||
|
|
import * as fs4 from "fs";
|
||
|
|
import * as path4 from "path";
|
||
|
|
|
||
|
|
// src/bytes.ts
|
||
|
|
class ByteReader {
|
||
|
|
src;
|
||
|
|
buf = new Uint8Array(0);
|
||
|
|
pos = 0;
|
||
|
|
done = !1;
|
||
|
|
constructor(src) {
|
||
|
|
this.src = src;
|
||
|
|
}
|
||
|
|
get buffered() {
|
||
|
|
return this.buf.length - this.pos;
|
||
|
|
}
|
||
|
|
async fill() {
|
||
|
|
if (this.done)
|
||
|
|
return !1;
|
||
|
|
let { value, done } = await this.src.next();
|
||
|
|
if (done || !value)
|
||
|
|
return this.done = !0, !1;
|
||
|
|
if (value.byteLength === 0)
|
||
|
|
return this.fill();
|
||
|
|
if (this.pos >= this.buf.length)
|
||
|
|
this.buf = value;
|
||
|
|
else
|
||
|
|
this.buf = Buffer.concat([this.buf.subarray(this.pos), value]);
|
||
|
|
return this.pos = 0, !0;
|
||
|
|
}
|
||
|
|
async readExact(n) {
|
||
|
|
while (this.buffered < n)
|
||
|
|
if (!await this.fill()) {
|
||
|
|
if (this.buffered === 0)
|
||
|
|
return null;
|
||
|
|
throw new XmacError(`unexpected end of stream (wanted ${n} bytes, had ${this.buffered})`);
|
||
|
|
}
|
||
|
|
let out = this.buf.subarray(this.pos, this.pos + n);
|
||
|
|
return this.pos += n, out;
|
||
|
|
}
|
||
|
|
async* readStream(n) {
|
||
|
|
let remaining = n;
|
||
|
|
while (remaining > 0) {
|
||
|
|
if (this.buffered === 0 && !await this.fill())
|
||
|
|
throw new XmacError(`unexpected end of stream (wanted ${remaining} more bytes)`);
|
||
|
|
let take = Math.min(this.buffered, remaining), out = this.buf.subarray(this.pos, this.pos + take);
|
||
|
|
this.pos += take, remaining -= take, yield out;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async skip(n) {
|
||
|
|
for await (let _ of this.readStream(n))
|
||
|
|
;
|
||
|
|
}
|
||
|
|
async atEof() {
|
||
|
|
if (this.buffered > 0)
|
||
|
|
return !1;
|
||
|
|
return !await this.fill();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/pbzx.ts
|
||
|
|
var XZ_MAGIC = Buffer.from([253, 55, 122, 88, 90, 0]);
|
||
|
|
async function* pbzxDecompress(source, onProgress) {
|
||
|
|
let reader = new ByteReader(source[Symbol.asyncIterator]()), magic = await reader.readExact(4);
|
||
|
|
if (!magic || (/* @__PURE__ */ new TextDecoder()).decode(magic) !== "pbzx")
|
||
|
|
throw new XmacError("payload: missing pbzx magic");
|
||
|
|
await reader.skip(8);
|
||
|
|
let consumed = 12, nextChunk = async () => {
|
||
|
|
let hdr = await reader.readExact(16);
|
||
|
|
if (hdr === null)
|
||
|
|
return null;
|
||
|
|
let b = asBuffer(hdr), uncompressedSize = Number(b.readBigUInt64BE(0)), compressedSize = Number(b.readBigUInt64BE(8));
|
||
|
|
if (compressedSize <= 0 || uncompressedSize < 0)
|
||
|
|
throw new XmacError("payload: corrupt pbzx chunk header");
|
||
|
|
return consumed += 16, {
|
||
|
|
uncompressedSize,
|
||
|
|
compressedSize,
|
||
|
|
raw: compressedSize === uncompressedSize
|
||
|
|
};
|
||
|
|
}, pending = await nextChunk();
|
||
|
|
while (pending !== null) {
|
||
|
|
if (pending.raw) {
|
||
|
|
for await (let piece of reader.readStream(pending.compressedSize))
|
||
|
|
consumed += piece.byteLength, onProgress?.(consumed), yield piece;
|
||
|
|
pending = await nextChunk();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let proc = Bun.spawn([requireTool("xz"), "-dc", "-T0"], {
|
||
|
|
stdin: "pipe",
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe"
|
||
|
|
}), writerError = null, writer = (async () => {
|
||
|
|
try {
|
||
|
|
while (pending !== null && !pending.raw) {
|
||
|
|
let first = !0;
|
||
|
|
for await (let piece of reader.readStream(pending.compressedSize)) {
|
||
|
|
if (first) {
|
||
|
|
if (first = !1, piece.byteLength >= 6 && Buffer.compare(piece.subarray(0, 6), XZ_MAGIC) !== 0)
|
||
|
|
throw new XmacError("payload: pbzx chunk is neither raw nor XZ \u2014 unsupported variant");
|
||
|
|
}
|
||
|
|
consumed += piece.byteLength, onProgress?.(consumed);
|
||
|
|
let r = proc.stdin.write(piece);
|
||
|
|
if (r && typeof r.then === "function")
|
||
|
|
await r;
|
||
|
|
await proc.stdin.flush();
|
||
|
|
}
|
||
|
|
pending = await nextChunk();
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
writerError = e;
|
||
|
|
try {
|
||
|
|
proc.kill();
|
||
|
|
} catch {}
|
||
|
|
} finally {
|
||
|
|
try {
|
||
|
|
await proc.stdin.end();
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
for await (let out of proc.stdout)
|
||
|
|
yield out;
|
||
|
|
await writer;
|
||
|
|
let code = await proc.exited;
|
||
|
|
if (writerError)
|
||
|
|
throw writerError;
|
||
|
|
if (code !== 0) {
|
||
|
|
let err = await new Response(proc.stderr).text();
|
||
|
|
throw new XmacError(`xz failed while decompressing payload: ${err.trim() || `exit ${code}`}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function* pipeThrough(cmd, source, onProgress) {
|
||
|
|
let proc = Bun.spawn(cmd, { stdin: "pipe", stdout: "pipe", stderr: "pipe" }), writerError = null, writer = (async () => {
|
||
|
|
try {
|
||
|
|
let n = 0;
|
||
|
|
for await (let piece of source) {
|
||
|
|
n += piece.byteLength, onProgress?.(n);
|
||
|
|
let r = proc.stdin.write(piece);
|
||
|
|
if (r && typeof r.then === "function")
|
||
|
|
await r;
|
||
|
|
await proc.stdin.flush();
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
writerError = e;
|
||
|
|
try {
|
||
|
|
proc.kill();
|
||
|
|
} catch {}
|
||
|
|
} finally {
|
||
|
|
try {
|
||
|
|
await proc.stdin.end();
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
for await (let out of proc.stdout)
|
||
|
|
yield out;
|
||
|
|
await writer;
|
||
|
|
let code = await proc.exited;
|
||
|
|
if (writerError)
|
||
|
|
throw writerError;
|
||
|
|
if (code !== 0) {
|
||
|
|
let err = await new Response(proc.stderr).text();
|
||
|
|
throw new XmacError(`${cmd[0]} failed while decompressing payload: ${err.trim() || `exit ${code}`}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function* decompressPayload(source, onProgress) {
|
||
|
|
let it = source[Symbol.asyncIterator](), first = await it.next();
|
||
|
|
if (first.done || !first.value)
|
||
|
|
throw new XmacError("payload: empty");
|
||
|
|
let head = first.value, rest = {
|
||
|
|
async* [Symbol.asyncIterator]() {
|
||
|
|
yield head;
|
||
|
|
while (!0) {
|
||
|
|
let n = await it.next();
|
||
|
|
if (n.done)
|
||
|
|
return;
|
||
|
|
yield n.value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if (head.byteLength >= 4 && (/* @__PURE__ */ new TextDecoder()).decode(head.subarray(0, 4)) === "pbzx") {
|
||
|
|
yield* pbzxDecompress(rest, onProgress);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (head[0] === 31 && head[1] === 139) {
|
||
|
|
yield* pipeThrough([requireTool("gzip"), "-dc"], rest, onProgress);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (head.byteLength >= 6 && Buffer.compare(head.subarray(0, 6), XZ_MAGIC) === 0) {
|
||
|
|
yield* pipeThrough([requireTool("xz"), "-dc", "-T0"], rest, onProgress);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
throw new XmacError(`payload: unrecognized format (first bytes: ${Buffer.from(head.subarray(0, 8)).toString("hex")})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/cpio.ts
|
||
|
|
var S_IFMT = 61440, S_IFDIR = 16384, S_IFREG = 32768, S_IFLNK = 40960;
|
||
|
|
function parseOctal(buf, off, len) {
|
||
|
|
let v = 0;
|
||
|
|
for (let i = 0;i < len; i++) {
|
||
|
|
let ch = buf[off + i];
|
||
|
|
if (ch === 32 || ch === 0)
|
||
|
|
continue;
|
||
|
|
v = v * 8 + (ch - 48);
|
||
|
|
}
|
||
|
|
return v;
|
||
|
|
}
|
||
|
|
function parseHex(buf, off, len) {
|
||
|
|
let v = 0;
|
||
|
|
for (let i = 0;i < len; i++) {
|
||
|
|
let ch = buf[off + i], d;
|
||
|
|
if (ch >= 48 && ch <= 57)
|
||
|
|
d = ch - 48;
|
||
|
|
else if (ch >= 65 && ch <= 70)
|
||
|
|
d = ch - 55;
|
||
|
|
else if (ch >= 97 && ch <= 102)
|
||
|
|
d = ch - 87;
|
||
|
|
else
|
||
|
|
continue;
|
||
|
|
v = v * 16 + d;
|
||
|
|
}
|
||
|
|
return v;
|
||
|
|
}
|
||
|
|
async function* cpioEntries(source) {
|
||
|
|
let reader = new ByteReader(source[Symbol.asyncIterator]()), dec = /* @__PURE__ */ new TextDecoder, offset = 0, readExact = async (n) => {
|
||
|
|
let b = await reader.readExact(n);
|
||
|
|
if (b !== null)
|
||
|
|
offset += n;
|
||
|
|
return b;
|
||
|
|
}, skipN = async (n) => {
|
||
|
|
if (n <= 0)
|
||
|
|
return;
|
||
|
|
await reader.skip(n), offset += n;
|
||
|
|
}, sawTrailer = !1;
|
||
|
|
while (!0) {
|
||
|
|
if (await reader.atEof())
|
||
|
|
return;
|
||
|
|
let magicBuf = await readExact(6);
|
||
|
|
if (magicBuf === null)
|
||
|
|
return;
|
||
|
|
let magic = dec.decode(magicBuf), mode, namesize, filesize, align = 1;
|
||
|
|
if (magic === "070707") {
|
||
|
|
let h = await readExact(70);
|
||
|
|
if (h === null)
|
||
|
|
throw new XmacError("cpio: truncated odc header");
|
||
|
|
mode = parseOctal(h, 12, 6), namesize = parseOctal(h, 53, 6), filesize = parseOctal(h, 59, 11);
|
||
|
|
} else if (magic === "070701" || magic === "070702") {
|
||
|
|
let h = await readExact(104);
|
||
|
|
if (h === null)
|
||
|
|
throw new XmacError("cpio: truncated newc header");
|
||
|
|
mode = parseHex(h, 8, 8), filesize = parseHex(h, 48, 8), namesize = parseHex(h, 88, 8), align = 4;
|
||
|
|
} else if (magicBuf.every((b) => b === 0)) {
|
||
|
|
while (!await reader.atEof())
|
||
|
|
await reader.skip(reader.buffered || 1);
|
||
|
|
return;
|
||
|
|
} else if (sawTrailer)
|
||
|
|
return;
|
||
|
|
else if (magicBuf[0] === 199 && magicBuf[1] === 113)
|
||
|
|
throw new XmacError("cpio: binary (bin/crc) cpio archives are not supported");
|
||
|
|
else
|
||
|
|
throw new XmacError(`cpio: bad magic '${asBuffer(magicBuf).toString("hex")}' at offset ${offset - 6}`);
|
||
|
|
let nameBuf = await readExact(namesize);
|
||
|
|
if (nameBuf === null)
|
||
|
|
throw new XmacError("cpio: truncated name");
|
||
|
|
let nameEnd = namesize;
|
||
|
|
while (nameEnd > 0 && nameBuf[nameEnd - 1] === 0)
|
||
|
|
nameEnd--;
|
||
|
|
let name = dec.decode(nameBuf.subarray(0, nameEnd));
|
||
|
|
if (align > 1)
|
||
|
|
await skipN((align - offset % align) % align);
|
||
|
|
if (name === "TRAILER!!!") {
|
||
|
|
await skipN(filesize), sawTrailer = !0;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let type = mode & S_IFMT, consumed = !1, linkTarget;
|
||
|
|
if (type === S_IFLNK && filesize > 0 && filesize < 8192) {
|
||
|
|
let t = await readExact(filesize);
|
||
|
|
if (t === null)
|
||
|
|
throw new XmacError("cpio: truncated symlink target");
|
||
|
|
if (linkTarget = dec.decode(t), align > 1)
|
||
|
|
await skipN((align - offset % align) % align);
|
||
|
|
consumed = !0;
|
||
|
|
}
|
||
|
|
let entry = {
|
||
|
|
path: name,
|
||
|
|
mode,
|
||
|
|
size: filesize,
|
||
|
|
linkTarget,
|
||
|
|
body: async function* () {
|
||
|
|
if (consumed)
|
||
|
|
return;
|
||
|
|
consumed = !0;
|
||
|
|
for await (let piece of reader.readStream(filesize))
|
||
|
|
offset += piece.byteLength, yield piece;
|
||
|
|
if (align > 1)
|
||
|
|
await skipN((align - offset % align) % align);
|
||
|
|
},
|
||
|
|
skip: async () => {
|
||
|
|
if (consumed)
|
||
|
|
return;
|
||
|
|
if (consumed = !0, await skipN(filesize), align > 1)
|
||
|
|
await skipN((align - offset % align) % align);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if (yield entry, !consumed)
|
||
|
|
await entry.skip();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/extract.ts
|
||
|
|
function newStats() {
|
||
|
|
return { files: 0, dirs: 0, symlinks: 0, bytes: 0, sdkNames: [] };
|
||
|
|
}
|
||
|
|
async function splatPackage(pkgPath, outDir, copySymlinks, stats) {
|
||
|
|
let xar = XarFile.open(pkgPath);
|
||
|
|
try {
|
||
|
|
let payload = xar.find("Payload");
|
||
|
|
if (!payload) {
|
||
|
|
let nested = xar.entries.find((e) => e.name.endsWith("/Payload"));
|
||
|
|
if (!nested)
|
||
|
|
throw new XmacError(`${path4.basename(pkgPath)}: no Payload entry found`);
|
||
|
|
throw new XmacError(`${path4.basename(pkgPath)}: nested product archives are not supported yet (found ${nested.name})`);
|
||
|
|
}
|
||
|
|
let prog = new Progress(path4.basename(pkgPath), payload.size), sdkRoot = path4.join(outDir, "SDKs");
|
||
|
|
fs4.mkdirSync(sdkRoot, { recursive: !0 });
|
||
|
|
let PREFIX = /^\.?\/?Library\/Developer\/CommandLineTools\/SDKs\//, ALT_PREFIX = /^\.?\/?.*?\/SDKs\/(?=[A-Za-z0-9_.]+\.sdk(\/|$))/, deferredLinks = [], symlinkFailures = 0, rejected = 0, inside = (p) => p === sdkRoot || p.startsWith(sdkRoot + path4.sep), safeLink = (at, target) => target !== "" && !path4.isAbsolute(target) && inside(path4.resolve(path4.dirname(at), target));
|
||
|
|
for await (let entry of cpioEntries(decompressPayload(xar.streamRaw(payload), (n) => prog.update(n)))) {
|
||
|
|
let rel;
|
||
|
|
if (PREFIX.test(entry.path))
|
||
|
|
rel = entry.path.replace(PREFIX, "");
|
||
|
|
else if (ALT_PREFIX.test(entry.path))
|
||
|
|
rel = entry.path.replace(ALT_PREFIX, "");
|
||
|
|
if (!rel || rel === "") {
|
||
|
|
await entry.skip();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let dest = path4.join(sdkRoot, rel);
|
||
|
|
if (!inside(dest)) {
|
||
|
|
rejected++, await entry.skip();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let type = entry.mode & S_IFMT, m = /^([A-Za-z0-9_.+-]+\.sdk)$/.exec(rel.replace(/\/$/, ""));
|
||
|
|
if (m && type === S_IFDIR && !stats.sdkNames.includes(m[1]))
|
||
|
|
stats.sdkNames.push(m[1]);
|
||
|
|
if (type === S_IFDIR)
|
||
|
|
fs4.mkdirSync(dest, { recursive: !0 }), stats.dirs++;
|
||
|
|
else if (type === S_IFLNK) {
|
||
|
|
let target = entry.linkTarget ?? "";
|
||
|
|
if (!safeLink(dest, target))
|
||
|
|
rejected++;
|
||
|
|
else if (copySymlinks)
|
||
|
|
deferredLinks.push({ at: dest, target });
|
||
|
|
else {
|
||
|
|
fs4.mkdirSync(path4.dirname(dest), { recursive: !0 });
|
||
|
|
try {
|
||
|
|
fs4.rmSync(dest, { force: !0 });
|
||
|
|
} catch {}
|
||
|
|
try {
|
||
|
|
fs4.symlinkSync(target, dest), stats.symlinks++;
|
||
|
|
} catch {
|
||
|
|
symlinkFailures++, deferredLinks.push({ at: dest, target });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (type === S_IFREG || type === 0) {
|
||
|
|
fs4.mkdirSync(path4.dirname(dest), { recursive: !0 });
|
||
|
|
let fd = fs4.openSync(dest, "w", entry.mode & 511 || 420);
|
||
|
|
try {
|
||
|
|
for await (let piece of entry.body()) {
|
||
|
|
let off = 0;
|
||
|
|
while (off < piece.byteLength)
|
||
|
|
off += fs4.writeSync(fd, piece, off, piece.byteLength - off, -1);
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
fs4.closeSync(fd);
|
||
|
|
}
|
||
|
|
stats.files++, stats.bytes += entry.size;
|
||
|
|
} else
|
||
|
|
await entry.skip();
|
||
|
|
}
|
||
|
|
if (deferredLinks.length > 0) {
|
||
|
|
let remaining = deferredLinks;
|
||
|
|
for (let pass = 0;pass < 40 && remaining.length > 0; pass++) {
|
||
|
|
let next = [];
|
||
|
|
for (let l of remaining) {
|
||
|
|
let resolved = path4.resolve(path4.dirname(l.at), l.target);
|
||
|
|
if (!inside(resolved))
|
||
|
|
continue;
|
||
|
|
try {
|
||
|
|
if (fs4.statSync(resolved).isDirectory())
|
||
|
|
fs4.cpSync(resolved, l.at, { recursive: !0, dereference: !0 });
|
||
|
|
else
|
||
|
|
fs4.mkdirSync(path4.dirname(l.at), { recursive: !0 }), fs4.copyFileSync(resolved, l.at);
|
||
|
|
stats.files++;
|
||
|
|
} catch {
|
||
|
|
next.push(l);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (next.length === remaining.length)
|
||
|
|
break;
|
||
|
|
remaining = next;
|
||
|
|
}
|
||
|
|
if (remaining.length > 0)
|
||
|
|
note(`${remaining.length} symlink(s) could not be materialized (dangling targets)`);
|
||
|
|
}
|
||
|
|
if (symlinkFailures > 0)
|
||
|
|
note(`${symlinkFailures} symlink(s) were materialized as copies because the filesystem rejected symlink creation`);
|
||
|
|
if (rejected > 0)
|
||
|
|
warn(`${rejected} entr${rejected === 1 ? "y" : "ies"} rejected for escaping the output directory (unexpected in a genuine Apple package)`);
|
||
|
|
prog.finish();
|
||
|
|
} finally {
|
||
|
|
xar.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/toolchain.ts
|
||
|
|
import * as fs5 from "fs";
|
||
|
|
import * as path5 from "path";
|
||
|
|
var ALL_ARCHS = ["arm64", "x86_64"];
|
||
|
|
function normalizeArch(a) {
|
||
|
|
let k = a.trim().toLowerCase();
|
||
|
|
if (k === "aarch64")
|
||
|
|
return "arm64";
|
||
|
|
if (k === "amd64")
|
||
|
|
return "x86_64";
|
||
|
|
if (k !== "arm64" && k !== "x86_64")
|
||
|
|
throw new XmacError(`unsupported --arch '${a}' (use arm64 and/or x86_64)`);
|
||
|
|
return k;
|
||
|
|
}
|
||
|
|
function cmakeToolchain(filePath, sdkRel, archs, minOs) {
|
||
|
|
return `# Generated by xmac ${VERSION} \u2014 cross-compile to ${archs.join("/")} macOS from Linux.
|
||
|
|
#
|
||
|
|
# cmake -B build -G Ninja --toolchain ${filePath}
|
||
|
|
#
|
||
|
|
# Requirements on the build host: clang and lld (for ld64.lld). Any
|
||
|
|
# clang >= 13 works; 15+ recommended. Override the compiler with
|
||
|
|
# -DXMAC_CLANG=/path/to/clang if the default is not what you want.
|
||
|
|
|
||
|
|
set(CMAKE_SYSTEM_NAME Darwin)
|
||
|
|
set(CMAKE_SYSTEM_PROCESSOR ${archs[0]})
|
||
|
|
|
||
|
|
get_filename_component(_xmac_root "\${CMAKE_CURRENT_LIST_DIR}" ABSOLUTE)
|
||
|
|
set(XMAC_SDK "\${_xmac_root}/${sdkRel}" CACHE PATH "macOS SDK root")
|
||
|
|
|
||
|
|
set(CMAKE_OSX_SYSROOT "\${XMAC_SDK}" CACHE PATH "" FORCE)
|
||
|
|
set(CMAKE_OSX_ARCHITECTURES "${archs.join(";")}" CACHE STRING "" FORCE)
|
||
|
|
set(CMAKE_OSX_DEPLOYMENT_TARGET "${minOs}" CACHE STRING "" FORCE)
|
||
|
|
set(CMAKE_MACOSX_RPATH ON)
|
||
|
|
|
||
|
|
# Locate clang/lld. Prefer an explicit override, then PATH.
|
||
|
|
if(NOT XMAC_CLANG)
|
||
|
|
find_program(XMAC_CLANG NAMES clang REQUIRED)
|
||
|
|
endif()
|
||
|
|
get_filename_component(_xmac_clang_dir "\${XMAC_CLANG}" DIRECTORY)
|
||
|
|
if(NOT XMAC_CLANGXX)
|
||
|
|
find_program(XMAC_CLANGXX NAMES clang++ HINTS "\${_xmac_clang_dir}" REQUIRED)
|
||
|
|
endif()
|
||
|
|
if(NOT XMAC_LLD)
|
||
|
|
find_program(XMAC_LLD NAMES ld64.lld HINTS "\${_xmac_clang_dir}")
|
||
|
|
endif()
|
||
|
|
if(NOT XMAC_LLD)
|
||
|
|
message(FATAL_ERROR "xmac: ld64.lld not found \u2014 install lld (apt-get install lld / brew install llvm)")
|
||
|
|
endif()
|
||
|
|
|
||
|
|
set(CMAKE_C_COMPILER "\${XMAC_CLANG}")
|
||
|
|
set(CMAKE_CXX_COMPILER "\${XMAC_CLANGXX}")
|
||
|
|
set(CMAKE_OBJC_COMPILER "\${XMAC_CLANG}")
|
||
|
|
set(CMAKE_OBJCXX_COMPILER "\${XMAC_CLANGXX}")
|
||
|
|
set(CMAKE_ASM_COMPILER "\${XMAC_CLANG}")
|
||
|
|
|
||
|
|
# A non-Apple clang defaults to the *build host's* triple; -arch alone is not
|
||
|
|
# enough to retarget it. CMake appends --target=<this> to every compile.
|
||
|
|
foreach(_lang C CXX OBJC OBJCXX ASM)
|
||
|
|
set(CMAKE_\${_lang}_COMPILER_TARGET "${archs[0]}-apple-macosx${minOs}")
|
||
|
|
endforeach()
|
||
|
|
|
||
|
|
# Use LLVM binutils when available so ranlib/ar understand Mach-O + fat files.
|
||
|
|
find_program(XMAC_AR NAMES llvm-ar HINTS "\${_xmac_clang_dir}")
|
||
|
|
find_program(XMAC_RANLIB NAMES llvm-ranlib HINTS "\${_xmac_clang_dir}")
|
||
|
|
find_program(XMAC_LIPO NAMES llvm-lipo HINTS "\${_xmac_clang_dir}")
|
||
|
|
find_program(XMAC_NM NAMES llvm-nm HINTS "\${_xmac_clang_dir}")
|
||
|
|
find_program(XMAC_STRIP NAMES llvm-strip HINTS "\${_xmac_clang_dir}")
|
||
|
|
find_program(XMAC_INSTALL_NAME_TOOL NAMES llvm-install-name-tool HINTS "\${_xmac_clang_dir}")
|
||
|
|
foreach(_t AR RANLIB NM STRIP)
|
||
|
|
if(XMAC_\${_t})
|
||
|
|
set(CMAKE_\${_t} "\${XMAC_\${_t}}" CACHE FILEPATH "" FORCE)
|
||
|
|
endif()
|
||
|
|
endforeach()
|
||
|
|
if(XMAC_INSTALL_NAME_TOOL)
|
||
|
|
set(CMAKE_INSTALL_NAME_TOOL "\${XMAC_INSTALL_NAME_TOOL}" CACHE FILEPATH "" FORCE)
|
||
|
|
endif()
|
||
|
|
|
||
|
|
# clang's Darwin driver invokes a system linker. Ask for the lld *flavor*
|
||
|
|
# (so the driver emits modern -platform_version arguments rather than the
|
||
|
|
# legacy flags ld64.lld no longer accepts) and add the directory containing
|
||
|
|
# ld64.lld as a program-search prefix so it is found even when it does not
|
||
|
|
# sit next to clang.
|
||
|
|
get_filename_component(_xmac_lld_dir "\${XMAC_LLD}" DIRECTORY)
|
||
|
|
# Plain set() rather than append: toolchain files are included several times
|
||
|
|
# per configure and appends would accumulate duplicates.
|
||
|
|
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld -B\${_xmac_lld_dir}")
|
||
|
|
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld -B\${_xmac_lld_dir}")
|
||
|
|
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld -B\${_xmac_lld_dir}")
|
||
|
|
|
||
|
|
# Use the SDK's libc++ headers, not the host clang's own copy (which targets
|
||
|
|
# the build machine and lacks a usable __config_site for Darwin). These are
|
||
|
|
# *_INIT variables: the project and the user can still append or override.
|
||
|
|
set(CMAKE_CXX_FLAGS_INIT "-stdlib++-isystem \${XMAC_SDK}/usr/include/c++/v1")
|
||
|
|
set(CMAKE_OBJCXX_FLAGS_INIT "-stdlib++-isystem \${XMAC_SDK}/usr/include/c++/v1")
|
||
|
|
|
||
|
|
# Search only the SDK, never the build host's headers/libraries.
|
||
|
|
set(CMAKE_FIND_ROOT_PATH "\${XMAC_SDK}")
|
||
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||
|
|
|
||
|
|
set(CMAKE_FRAMEWORK_PATH "\${XMAC_SDK}/System/Library/Frameworks")
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
function emitToolchain(outDir, sdkName, archs, minOs) {
|
||
|
|
let sdkRel = `SDKs/${sdkName}`, binDir = path5.join(outDir, "bin");
|
||
|
|
fs5.mkdirSync(binDir, { recursive: !0 });
|
||
|
|
let cmakeAll = [], writeCmake = (fileName, fileArchs) => {
|
||
|
|
let p = path5.join(outDir, fileName);
|
||
|
|
return fs5.writeFileSync(p, cmakeToolchain(p, sdkRel, fileArchs, minOs)), cmakeAll.push(p), p;
|
||
|
|
}, cmakePath = "";
|
||
|
|
for (let arch of ALL_ARCHS) {
|
||
|
|
let p = writeCmake(`${arch}-apple-darwin.toolchain.cmake`, [arch]);
|
||
|
|
if (archs[0] === arch)
|
||
|
|
cmakePath = p;
|
||
|
|
}
|
||
|
|
if (archs.length > 1)
|
||
|
|
cmakePath = writeCmake("universal-apple-darwin.toolchain.cmake", archs);
|
||
|
|
let primaryArch = archs[0], archFlags = archs.map((a) => `-arch ${a}`).join(" "), envPath = path5.join(outDir, "env.sh"), env = `# Generated by xmac ${VERSION} \u2014 source this for non-CMake builds.
|
||
|
|
#
|
||
|
|
# source ${envPath}
|
||
|
|
# $CC -o hello hello.c
|
||
|
|
#
|
||
|
|
# Works with make, ninja (via CC/CXX in your generator), autotools
|
||
|
|
# (./configure --host=${primaryArch}-apple-darwin), cargo (see below), etc.
|
||
|
|
|
||
|
|
_xmac_root="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||
|
|
export SDKROOT="$_xmac_root/${sdkRel}"
|
||
|
|
export MACOSX_DEPLOYMENT_TARGET="${minOs}"
|
||
|
|
|
||
|
|
_xmac_lld="$(command -v ld64.lld || true)"
|
||
|
|
if [ -z "$_xmac_lld" ]; then
|
||
|
|
echo "xmac: warning: ld64.lld not found on PATH; linking will fail (install lld)" >&2
|
||
|
|
fi
|
||
|
|
|
||
|
|
export XMAC_CFLAGS="--sysroot=$SDKROOT ${archFlags} -mmacosx-version-min=${minOs}"
|
||
|
|
export XMAC_CXXFLAGS="$XMAC_CFLAGS -stdlib++-isystem $SDKROOT/usr/include/c++/v1"
|
||
|
|
export XMAC_LDFLAGS="-fuse-ld=lld -B$(dirname "\${_xmac_lld:-/usr/bin/ld64.lld}") -Wl,-syslibroot,$SDKROOT"
|
||
|
|
|
||
|
|
export CC="$_xmac_root/bin/${primaryArch}-apple-darwin-cc"
|
||
|
|
export CXX="$_xmac_root/bin/${primaryArch}-apple-darwin-c++"
|
||
|
|
export AR="$(command -v llvm-ar || command -v ar)"
|
||
|
|
export RANLIB="$(command -v llvm-ranlib || command -v ranlib)"
|
||
|
|
|
||
|
|
# Cargo / Rust convenience (requires \`rustup target add <triple>-apple-darwin\`):
|
||
|
|
${archs.map((a) => {
|
||
|
|
let rustArch = a === "arm64" ? "aarch64" : a;
|
||
|
|
return `export CARGO_TARGET_${rustArch.toUpperCase()}_APPLE_DARWIN_LINKER="$_xmac_root/bin/${a}-apple-darwin-cc"
|
||
|
|
export CC_${rustArch}_apple_darwin="$_xmac_root/bin/${a}-apple-darwin-cc"
|
||
|
|
export CXX_${rustArch}_apple_darwin="$_xmac_root/bin/${a}-apple-darwin-c++"`;
|
||
|
|
}).join(`
|
||
|
|
`)}
|
||
|
|
`;
|
||
|
|
fs5.writeFileSync(envPath, env);
|
||
|
|
let wrappers = [];
|
||
|
|
for (let arch of ALL_ARCHS)
|
||
|
|
for (let [tool, driver] of [
|
||
|
|
["cc", "clang"],
|
||
|
|
["c++", "clang++"]
|
||
|
|
]) {
|
||
|
|
let isCxx = tool === "c++", wrapper = `#!/bin/sh
|
||
|
|
# Generated by xmac ${VERSION}. Cross-compile ${isCxx ? "C++/ObjC++" : "C/ObjC"} to ${arch}-apple-macosx.
|
||
|
|
here="$(cd "$(dirname "$0")/.." && pwd)"
|
||
|
|
sdk="$here/${sdkRel}"
|
||
|
|
lld="\${XMAC_LLD:-$(command -v ld64.lld)}"
|
||
|
|
if [ -z "$lld" ]; then
|
||
|
|
echo "xmac: ld64.lld not found on PATH (install lld)" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
exec "\${XMAC_${driver === "clang" ? "CLANG" : "CLANGXX"}:-${driver}}" \\
|
||
|
|
--target=${arch}-apple-macosx\${MACOSX_DEPLOYMENT_TARGET:-${minOs}} \\
|
||
|
|
-isysroot "$sdk" \\
|
||
|
|
-fuse-ld=lld \\
|
||
|
|
-B "$(dirname "$lld")" \\
|
||
|
|
-Wl,-syslibroot,"$sdk" \\${isCxx ? `
|
||
|
|
-stdlib++-isystem "$sdk/usr/include/c++/v1" \\` : ""}
|
||
|
|
"$@"
|
||
|
|
`, p = path5.join(binDir, `${arch}-apple-darwin-${tool}`);
|
||
|
|
fs5.writeFileSync(p, wrapper), fs5.chmodSync(p, 493), wrappers.push(p);
|
||
|
|
}
|
||
|
|
return { cmake: cmakePath, cmakeAll, env: envPath, wrappers };
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/commands.ts
|
||
|
|
var LICENSE_NOTICE = `The macOS SDK is licensed by Apple Inc. under the "macOS SDK and Xcode
|
||
|
|
Agreement" (and, for SDKs obtained through Xcode, the Xcode and Apple SDKs
|
||
|
|
Agreement). xmac downloads the SDK directly from Apple's servers to *this*
|
||
|
|
machine \u2014 it does not redistribute anything \u2014 but by extracting and using the
|
||
|
|
SDK you are agreeing to Apple's license terms, which notably restrict the SDK
|
||
|
|
to developing software for Apple platforms and prohibit redistributing the SDK
|
||
|
|
itself (e.g. do not commit the extracted SDK to a public repository or bake it
|
||
|
|
into a public container image).
|
||
|
|
|
||
|
|
View the full text with \`xmac license --release <ver>\` or at:
|
||
|
|
https://www.apple.com/legal/sla/
|
||
|
|
|
||
|
|
Pass --accept-license to proceed.`;
|
||
|
|
function checkLicense(accepted) {
|
||
|
|
if (accepted)
|
||
|
|
return;
|
||
|
|
if (process.stderr.write(LICENSE_NOTICE + `
|
||
|
|
`), process.stdin.isTTY && process.stderr.isTTY) {
|
||
|
|
let answer = prompt(`
|
||
|
|
Do you accept the license terms? [y/N]`);
|
||
|
|
if (answer && /^y(es)?$/i.test(answer.trim()))
|
||
|
|
return;
|
||
|
|
die("license not accepted");
|
||
|
|
}
|
||
|
|
die("pass --accept-license to accept Apple's license terms non-interactively");
|
||
|
|
}
|
||
|
|
async function cmdList(opts, refresh) {
|
||
|
|
let manifest = await getManifest(opts, refresh);
|
||
|
|
if (opts.json) {
|
||
|
|
console.log(JSON.stringify(manifest, null, 2));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
console.log(c.bold("Command Line Tools releases from Apple's software-update catalog")), console.log(c.dim(`catalog fetched ${manifest.fetchedAt}`)), console.log("");
|
||
|
|
let rows = [["CLT", "RELEASED", "PRODUCT", "SDKS"]];
|
||
|
|
for (let r of manifest.releases) {
|
||
|
|
let sdks = r.sdkPackages.map((s) => `${interactive ? c.cyan(s.sdkName?.replace(/\.sdk$/, "") ?? s.fileName) : s.sdkName?.replace(/\.sdk$/, "") ?? s.fileName} ${c.dim(`(${humanSize(s.size)})`)}`).join(", ");
|
||
|
|
rows.push([r.version, r.postDate, r.productId, sdks]);
|
||
|
|
}
|
||
|
|
table(rows), console.log(""), console.log(c.dim("Use `xmac splat --accept-license --sdk <version>` to fetch one."));
|
||
|
|
}
|
||
|
|
async function cmdLicense(opts, releaseSpec) {
|
||
|
|
let manifest = await getManifest(opts);
|
||
|
|
if (manifest.releases.length === 0)
|
||
|
|
die("no Command Line Tools releases found in the catalog");
|
||
|
|
let release = releaseSpec ? selectSdks(manifest, "all", releaseSpec).release : manifest.releases[0], text = await fetchDistribution(release), m = /<license[^>]*>([\s\S]*?)<\/license>/.exec(text);
|
||
|
|
if (!m)
|
||
|
|
die("could not locate the license text in the distribution file");
|
||
|
|
let plain = decodeEntities(m[1]).replace(/\{\\(?:fonttbl|colortbl|\*\\[a-z]+)[^{}]*\}/g, "").replace(/\\'([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\par[d]?\b/g, `
|
||
|
|
`).replace(/\\tab\b/g, "\t").replace(/\\[a-zA-Z]+-?\d* ?/g, "").replace(/[{}]/g, "").replace(/^[\s;]+/, "").replace(/\n{3,}/g, `
|
||
|
|
|
||
|
|
`).trim();
|
||
|
|
console.log(c.bold(`# License for ${displayName(release)}`)), console.log(""), console.log(plain);
|
||
|
|
}
|
||
|
|
function pkgCachePath(opts, release, pkg) {
|
||
|
|
return path6.join(opts.cacheDir, "dl", release.productId, pkg.fileName);
|
||
|
|
}
|
||
|
|
async function cmdDownload(opts, sel) {
|
||
|
|
let paths = [];
|
||
|
|
status(`Downloading ${sel.packages.length} package(s) for ${displayName(sel.release)}`);
|
||
|
|
for (let pkg of sel.packages) {
|
||
|
|
let dest = pkgCachePath(opts, sel.release, pkg);
|
||
|
|
if (opts.offline) {
|
||
|
|
if (!fs6.existsSync(dest))
|
||
|
|
throw new XmacError(`--offline: ${dest} is not in the cache`);
|
||
|
|
} else
|
||
|
|
await downloadTo(pkg.url, dest, pkg.size, void 0, `${pkg.sdkName ?? pkg.fileName}`);
|
||
|
|
paths.push(dest);
|
||
|
|
}
|
||
|
|
return paths;
|
||
|
|
}
|
||
|
|
async function cmdUnpack(opts, sel) {
|
||
|
|
let pkgPaths = await cmdDownload(opts, sel), results = [];
|
||
|
|
for (let p of pkgPaths) {
|
||
|
|
let dest = p.replace(/\.pkg$/i, "") + ".unpacked";
|
||
|
|
fs6.mkdirSync(dest, { recursive: !0 });
|
||
|
|
let stats = newStats();
|
||
|
|
status(`Unpacking ${path6.basename(p)}`), await splatPackage(p, dest, !1, stats), ok(`${path6.basename(p)}: ${thousands(stats.files)} files, ${humanSize(stats.bytes)} (${stats.sdkNames.join(", ") || "no SDK found"})`);
|
||
|
|
for (let n of stats.sdkNames)
|
||
|
|
results.push([n.replace(/\.sdk$/, ""), path6.join(dest, "SDKs", n)]);
|
||
|
|
}
|
||
|
|
result(results.map(([k, v]) => [`sdk-path[${k}]`, v]));
|
||
|
|
}
|
||
|
|
function readSdkSettings(outDir, sdkName) {
|
||
|
|
try {
|
||
|
|
let s = JSON.parse(fs6.readFileSync(path6.join(outDir, "SDKs", sdkName, "SDKSettings.json"), "utf8"));
|
||
|
|
return { display: s.DisplayName, version: s.Version };
|
||
|
|
} catch {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function cmdSplat(opts, sel, splat) {
|
||
|
|
let pkgPaths = await cmdDownload(opts, sel), outDir = path6.resolve(splat.output);
|
||
|
|
fs6.mkdirSync(outDir, { recursive: !0 });
|
||
|
|
let stats = newStats();
|
||
|
|
status(`Extracting into ${outDir}`);
|
||
|
|
for (let p of pkgPaths)
|
||
|
|
await splatPackage(p, outDir, splat.copySymlinks, stats);
|
||
|
|
if (stats.sdkNames.length === 0)
|
||
|
|
throw new XmacError("no MacOSX*.sdk directory was found in the package payload(s)");
|
||
|
|
let named = stats.sdkNames.map((n) => ({ n, v: /MacOSX(\d+(?:\.\d+)*)\.sdk/i.exec(n)?.[1] })).sort((a, b) => compareVersions(b.v ?? "0", a.v ?? "0")), mainSdk = named[0].n, emitted;
|
||
|
|
if (!splat.sdkOnly)
|
||
|
|
emitted = emitToolchain(outDir, mainSdk, splat.archs, splat.minOs);
|
||
|
|
for (let { n, v } of named) {
|
||
|
|
if (!v)
|
||
|
|
continue;
|
||
|
|
let major = v.split(".")[0];
|
||
|
|
for (let alias of [`MacOSX${major}.sdk`, "MacOSX.sdk"]) {
|
||
|
|
let aliasPath = path6.join(outDir, "SDKs", alias);
|
||
|
|
if (!fs6.existsSync(aliasPath) && !stats.sdkNames.includes(alias))
|
||
|
|
try {
|
||
|
|
fs6.symlinkSync(n, aliasPath);
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let settings = readSdkSettings(outDir, mainSdk);
|
||
|
|
if (ok(`${c.bold(mainSdk)}${settings.display ? ` ${c.dim(`(${settings.display})`)}` : ""} \u2014 ${thousands(stats.files)} files, ${thousands(stats.dirs)} directories, ${thousands(stats.symlinks)} symlinks, ${humanSize(stats.bytes)}`), interactive)
|
||
|
|
console.log("");
|
||
|
|
let pairs = [];
|
||
|
|
for (let { n } of named) {
|
||
|
|
let s = readSdkSettings(outDir, n);
|
||
|
|
if (pairs.push([named.length > 1 ? `sdk[${n}]` : "sdk", n]), s.version)
|
||
|
|
pairs.push([named.length > 1 ? `sdk-version[${n}]` : "sdk-version", s.version]);
|
||
|
|
}
|
||
|
|
if (pairs.push(["sdk-path", path6.join(outDir, "SDKs", mainSdk)]), pairs.push(["sdk-root", path6.join(outDir, "SDKs")]), emitted)
|
||
|
|
pairs.push(["toolchain-cmake", emitted.cmake]), pairs.push(["env-script", emitted.env]), pairs.push(["cc", path6.join(outDir, "bin", `${splat.archs[0]}-apple-darwin-cc`)]), pairs.push(["cxx", path6.join(outDir, "bin", `${splat.archs[0]}-apple-darwin-c++`)]);
|
||
|
|
if (pairs.push(["files", String(stats.files)]), pairs.push(["bytes", String(stats.bytes)]), result(pairs), interactive)
|
||
|
|
console.log(""), console.log(c.bold("Cross-compile with CMake:")), console.log(c.dim(` cmake -B build -G Ninja --toolchain ${emitted?.cmake ?? `<output>/${splat.archs[0]}-apple-darwin.toolchain.cmake`}`)), console.log(c.bold("Or anything else (make, ninja, cargo, ./configure):")), console.log(c.dim(` source ${emitted?.env ?? "<output>/env.sh"} && $CC -o hello hello.c`)), console.log(c.bold("Or directly:")), console.log(c.dim(` clang --target=${splat.archs[0]}-apple-macosx${splat.minOs} -isysroot ${path6.join(outDir, "SDKs", mainSdk)} -fuse-ld=lld ...`));
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/main.ts
|
||
|
|
var HELP = `xmac ${VERSION}
|
||
|
|
Download and extract macOS SDKs from Apple's public CDN for cross-compilation.
|
||
|
|
|
||
|
|
USAGE:
|
||
|
|
xmac [GLOBAL OPTIONS] <COMMAND> [OPTIONS]
|
||
|
|
|
||
|
|
COMMANDS:
|
||
|
|
list List Command Line Tools releases and the SDKs they contain
|
||
|
|
download Download SDK package(s) into the cache
|
||
|
|
unpack Download + extract package payloads into the cache
|
||
|
|
splat Download + unpack + emit an SDK and cross-compilation toolchain
|
||
|
|
license Print Apple's license terms for a release
|
||
|
|
clean Remove the cache directory
|
||
|
|
|
||
|
|
GLOBAL OPTIONS:
|
||
|
|
--cache-dir <DIR> Cache directory [default: ./.xmac-cache]
|
||
|
|
--catalog <URL> Apple software-update catalog URL to use
|
||
|
|
--offline Never hit the network; fail if the cache is missing
|
||
|
|
--json Machine-readable output (list)
|
||
|
|
-q, --quiet Suppress progress output
|
||
|
|
-h, --help Show this help
|
||
|
|
-V, --version Show version
|
||
|
|
|
||
|
|
SELECTION OPTIONS (download / unpack / splat):
|
||
|
|
--sdk <SPEC> Which SDK to fetch. One of:
|
||
|
|
latest newest SDK available (default)
|
||
|
|
<major[.minor]> e.g. "15", "15.2", "MacOSX14.5"
|
||
|
|
all every SDK in the chosen release
|
||
|
|
--release <VER|ID> Pin a Command Line Tools release (e.g. "16.2" or a
|
||
|
|
product id like "072-44426") instead of searching all
|
||
|
|
--accept-license Accept Apple's license terms non-interactively.
|
||
|
|
Required in CI. See \`xmac license\`.
|
||
|
|
|
||
|
|
SPLAT OPTIONS:
|
||
|
|
--output <DIR> Output directory [default: ./xmac-sdk]
|
||
|
|
--arch <LIST> Default target arch for the toolchain files: arm64,
|
||
|
|
x86_64, or arm64,x86_64 for universal binaries
|
||
|
|
[default: arm64]. Wrapper scripts for every arch are
|
||
|
|
always emitted; this only sets the default.
|
||
|
|
--min-os <VER> Deployment target baked into toolchain files
|
||
|
|
[default: 11.0]
|
||
|
|
--sdk-only Only emit the .sdk directory, no toolchain files
|
||
|
|
--copy-symlinks Materialize symlinks as file copies (for filesystems
|
||
|
|
or tools that cannot handle symlinks)
|
||
|
|
|
||
|
|
OUTPUT:
|
||
|
|
Progress and status go to stderr. Final results go to stdout as stable
|
||
|
|
\`key: value\` lines when stdout is not a terminal, e.g.:
|
||
|
|
SDKROOT=$(xmac splat --accept-license -q | awk '/^sdk-path:/{print $2}')
|
||
|
|
|
||
|
|
EXAMPLES:
|
||
|
|
# See what Apple is currently serving
|
||
|
|
xmac list
|
||
|
|
|
||
|
|
# CI one-liner: newest SDK + toolchain into ./xmac-sdk
|
||
|
|
xmac splat --accept-license
|
||
|
|
|
||
|
|
# A specific SDK, universal toolchain
|
||
|
|
xmac splat --accept-license --sdk 14 --arch arm64,x86_64 --output /opt/mac
|
||
|
|
|
||
|
|
# Then cross-compile:
|
||
|
|
cmake -B build -G Ninja --toolchain /opt/mac/arm64-apple-darwin.toolchain.cmake
|
||
|
|
# ...or without CMake:
|
||
|
|
source /opt/mac/env.sh && $CC hello.c -o hello
|
||
|
|
`, FLAGS_WITH_VALUES = /* @__PURE__ */ new Set([
|
||
|
|
"--cache-dir",
|
||
|
|
"--catalog",
|
||
|
|
"--sdk",
|
||
|
|
"--release",
|
||
|
|
"--output",
|
||
|
|
"--arch",
|
||
|
|
"--min-os"
|
||
|
|
]), KNOWN_BOOL_FLAGS = /* @__PURE__ */ new Set([
|
||
|
|
"--offline",
|
||
|
|
"--json",
|
||
|
|
"--quiet",
|
||
|
|
"-q",
|
||
|
|
"--help",
|
||
|
|
"-h",
|
||
|
|
"--version",
|
||
|
|
"-V",
|
||
|
|
"--accept-license",
|
||
|
|
"--sdk-only",
|
||
|
|
"--copy-symlinks",
|
||
|
|
"--preserve-symlinks",
|
||
|
|
"--refresh"
|
||
|
|
]);
|
||
|
|
function parseArgs(argv) {
|
||
|
|
let out = { flags: /* @__PURE__ */ new Map, positional: [] };
|
||
|
|
for (let i = 0;i < argv.length; i++) {
|
||
|
|
let a = argv[i];
|
||
|
|
if (a === "--") {
|
||
|
|
out.positional.push(...argv.slice(i + 1));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (a.startsWith("--")) {
|
||
|
|
let eq = a.indexOf("=");
|
||
|
|
if (eq !== -1) {
|
||
|
|
let key = a.slice(0, eq);
|
||
|
|
if (!FLAGS_WITH_VALUES.has(key))
|
||
|
|
die(`unknown option '${key}'. Run \`xmac --help\`.`);
|
||
|
|
out.flags.set(key, a.slice(eq + 1));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (FLAGS_WITH_VALUES.has(a)) {
|
||
|
|
let v = argv[++i];
|
||
|
|
if (v === void 0)
|
||
|
|
die(`${a} requires a value`);
|
||
|
|
out.flags.set(a, v);
|
||
|
|
} else if (KNOWN_BOOL_FLAGS.has(a))
|
||
|
|
out.flags.set(a, !0);
|
||
|
|
else
|
||
|
|
die(`unknown option '${a}'. Run \`xmac --help\`.`);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (a.startsWith("-") && a.length > 1) {
|
||
|
|
if (!KNOWN_BOOL_FLAGS.has(a))
|
||
|
|
die(`unknown option '${a}'. Run \`xmac --help\`.`);
|
||
|
|
out.flags.set(a, !0);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (!out.command)
|
||
|
|
out.command = a;
|
||
|
|
else
|
||
|
|
out.positional.push(a);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
async function main() {
|
||
|
|
let args = parseArgs(process.argv.slice(2));
|
||
|
|
if (args.flags.has("-V") || args.flags.has("--version")) {
|
||
|
|
console.log(`xmac ${VERSION}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (args.flags.has("-h") || args.flags.has("--help") || !args.command) {
|
||
|
|
console.log(HELP);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
setQuiet(Boolean(args.flags.get("-q") || args.flags.get("--quiet")));
|
||
|
|
let opts = {
|
||
|
|
cacheDir: path7.resolve(String(args.flags.get("--cache-dir") ?? process.env.XMAC_CACHE_DIR ?? "./.xmac-cache")),
|
||
|
|
catalog: String(args.flags.get("--catalog") ?? process.env.XMAC_CATALOG ?? DEFAULT_SUCATALOG),
|
||
|
|
offline: Boolean(args.flags.get("--offline")),
|
||
|
|
json: Boolean(args.flags.get("--json"))
|
||
|
|
};
|
||
|
|
if (!["list", "download", "unpack", "splat", "clean", "license"].includes(args.command))
|
||
|
|
die(`unknown command '${args.command}'. Run \`xmac --help\`.`);
|
||
|
|
if (args.command === "clean") {
|
||
|
|
fs7.rmSync(opts.cacheDir, { recursive: !0, force: !0 }), log(`removed ${opts.cacheDir}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (args.command === "list") {
|
||
|
|
await cmdList(opts, Boolean(args.flags.get("--refresh")));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (args.command === "license") {
|
||
|
|
await cmdLicense(opts, args.flags.get("--release") ?? args.positional[0]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
checkLicense(Boolean(args.flags.get("--accept-license")));
|
||
|
|
let manifest = await getManifest(opts), sel = selectSdks(manifest, String(args.flags.get("--sdk") ?? "latest"), args.flags.get("--release"));
|
||
|
|
if (status(`Selected ${displayName(sel.release)} (${sel.release.productId}, ${sel.release.postDate}): ${sel.packages.map((p) => p.sdkName ?? p.fileName).join(", ")}`), args.command === "download") {
|
||
|
|
await cmdDownload(opts, sel);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (args.command === "unpack") {
|
||
|
|
await cmdUnpack(opts, sel);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (args.command === "splat") {
|
||
|
|
let archs = String(args.flags.get("--arch") ?? "arm64").split(",").filter((a) => a.trim() !== "").map(normalizeArch);
|
||
|
|
if (archs.length === 0)
|
||
|
|
die("--arch requires at least one architecture");
|
||
|
|
await cmdSplat(opts, sel, {
|
||
|
|
output: String(args.flags.get("--output") ?? "./xmac-sdk"),
|
||
|
|
archs: [...new Set(archs)],
|
||
|
|
minOs: String(args.flags.get("--min-os") ?? "11.0"),
|
||
|
|
sdkOnly: Boolean(args.flags.get("--sdk-only")),
|
||
|
|
copySymlinks: Boolean(args.flags.get("--copy-symlinks"))
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function run() {
|
||
|
|
main().catch((e) => {
|
||
|
|
if (e instanceof XmacError)
|
||
|
|
die(e.message);
|
||
|
|
console.error(e), process.exit(1);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// xmac.ts
|
||
|
|
run();
|