Compare commits

...

10 Commits

Author SHA1 Message Date
Meghan Denny
85c93db9e8 [skip ci] 2025-11-26 22:00:29 -08:00
Meghan Denny
06d2ef5668 [build images] 2025-11-26 20:46:56 -08:00
autofix-ci[bot]
270c23fd87 [autofix.ci] apply automated fixes 2025-11-27 04:46:01 +00:00
Meghan Denny
6d73e8ed59 [build images] 2025-11-26 20:44:20 -08:00
Meghan Denny
caac13dd21 [build images] 2025-11-26 20:40:11 -08:00
Meghan Denny
9a242d10a2 [build images] 2025-11-26 20:20:35 -08:00
Meghan Denny
eb0bafaaca [build images] 2025-11-26 20:13:37 -08:00
autofix-ci[bot]
873d56b40d [autofix.ci] apply automated fixes 2025-11-27 04:12:31 +00:00
Meghan Denny
4f89afd183 [build images] 2025-11-26 20:10:49 -08:00
Meghan Denny
a2fa9a5808 ci: unregress tart image gen for macos 2025-11-26 20:08:37 -08:00
6 changed files with 165 additions and 107 deletions

View File

@@ -103,7 +103,10 @@ function getTargetLabel(target) {
* @type {Platform[]}
*/
const buildPlatforms = [
{ os: "darwin", arch: "aarch64", release: "13" },
{ os: "darwin", arch: "aarch64", release: "14" },
{ os: "darwin", arch: "aarch64", release: "15" },
{ os: "darwin", arch: "aarch64", release: "26" },
{ os: "darwin", arch: "x64", release: "14" },
{ os: "linux", arch: "aarch64", distro: "amazonlinux", release: "2023", features: ["docker"] },
{ os: "linux", arch: "x64", distro: "amazonlinux", release: "2023", features: ["docker"] },
@@ -612,6 +615,7 @@ function getBuildImageStep(platform, options) {
const { os, arch, distro, release, features } = platform;
const { publishImages } = options;
const action = publishImages ? "publish-image" : "create-image";
const cloud = os == "darwin" ? "tart" : "aws";
const command = [
"node",
@@ -621,7 +625,7 @@ function getBuildImageStep(platform, options) {
`--arch=${arch}`,
distro && `--distro=${distro}`,
`--release=${release}`,
"--cloud=aws",
`--cloud=${cloud}`,
"--ci",
"--authorized-org=oven-sh",
];
@@ -660,7 +664,9 @@ function getReleaseStep(buildPlatforms, options) {
agents: {
queue: "test-darwin",
},
depends_on: buildPlatforms.filter(p => p.os !== "freebsd").map(platform => `${getTargetKey(platform)}-build-bun`),
depends_on: buildPlatforms
.filter(p => !(p.os === "freebsd" || p.os === "darwin"))
.map(platform => `${getTargetKey(platform)}-build-bun`),
env: {
CANARY: revision,
},
@@ -1073,7 +1079,7 @@ async function getPipeline(options = {}) {
const imagePlatforms = new Map(
buildImages || publishImages
? [...buildPlatforms, ...testPlatforms]
.filter(({ os }) => os !== "darwin")
.filter(({ os, arch }) => !(os === "darwin"))
.map(platform => [getImageKey(platform), platform])
: [],
);

View File

@@ -92,6 +92,10 @@
"machine:linux:amazonlinux": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=amazonlinux --release=2023",
"machine:windows:2019": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=windows --release=2019",
"machine:freebsd": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=freebsd --release=14.3",
"machine:macos:13": "./scripts/machine.mjs ssh --cloud=tart --arch=arm64 --os=darwin --distro=macos --release=13",
"machine:macos:14": "./scripts/machine.mjs ssh --cloud=tart --arch=arm64 --os=darwin --distro=macos --release=14",
"machine:macos:15": "./scripts/machine.mjs ssh --cloud=tart --arch=arm64 --os=darwin --distro=macos --release=15",
"machine:macos:26": "./scripts/machine.mjs ssh --cloud=tart --arch=arm64 --os=darwin --distro=macos --release=26",
"sync-webkit-source": "bun ./scripts/sync-webkit-source.ts"
}
}

View File

@@ -22,15 +22,14 @@ error() {
if ! [ "$$" = "$pid" ]; then
kill -s TERM "$pid"
fi
exit 1
# Kill the shell. This used to be 'exit 1' but if the command is running inside a
# subshell, then only the subshell dies and the script keeps running uninterrupted.
kill $$
}
execute() {
local opts=$-
set -x
"$@"
{ local status=$?; set +x "$opts"; } 2> /dev/null
if [ "$status" -ne 0 ]; then
print "$ $@" >&2
if ! "$@"; then
error "Command failed: $@"
fi
}
@@ -62,7 +61,7 @@ execute_as_user() {
}
grant_to_user() {
path="$1"
local path="$1"
if ! [ -f "$path" ] && ! [ -d "$path" ]; then
error "Could not find file or directory: \"$path\""
fi
@@ -77,7 +76,7 @@ which() {
}
require() {
path="$(which "$1")"
local path="$(which "$1")"
if ! [ -f "$path" ]; then
error "Command \"$1\" is required, but is not installed."
fi
@@ -109,7 +108,7 @@ compare_version() {
}
create_directory() {
path="$1"
local path="$1"
path_dir="$path"
while ! [ -d "$path_dir" ]; do
path_dir="$(dirname "$path_dir")"
@@ -132,13 +131,13 @@ create_directory() {
create_tmp_directory() {
mktemp="$(require mktemp)"
path="$(execute "$mktemp" -d)"
local path="$(execute "$mktemp" -d)"
grant_to_user "$path"
print "$path"
}
create_file() {
path="$1"
local path="$1"
path_dir="$(dirname "$path")"
if ! [ -d "$path_dir" ]; then
create_directory "$path_dir"
@@ -164,7 +163,7 @@ create_file() {
}
append_file() {
path="$1"
local path="$1"
if ! [ -f "$path" ]; then
create_file "$path"
fi
@@ -203,7 +202,7 @@ download_and_verify_file() {
file_url="$1"
hash="$2"
path=$(download_file "$file_url")
local path=$(download_file "$file_url")
execute sh -c 'echo "'"$hash $path"'" | sha256sum -c -' >/dev/null 2>&1
print "$path"
@@ -222,7 +221,7 @@ append_to_profile() {
}
append_to_path() {
path="$1"
local path="$1"
if ! [ -d "$path" ]; then
error "Could not find directory: \"$path\""
fi
@@ -331,7 +330,7 @@ check_operating_system() {
darwin)
sw_vers="$(which sw_vers)"
if [ -f "$sw_vers" ]; then
distro="$("$sw_vers" -productName)"
distro="$("$sw_vers" -productName | tr '[:upper:]' '[:lower:]')"
release="$("$sw_vers" -productVersion)"
fi
@@ -372,12 +371,12 @@ check_operating_system() {
;;
esac
fi
if [ -n "$abi" ]; then
print "ABI: $abi $abi_version"
fi
;;
esac
if [ -n "$abi" ]; then
print "ABI: $abi $abi_version"
fi
}
check_inside_docker() {
@@ -456,6 +455,9 @@ check_package_manager() {
export ASSUME_ALWAYS_YES=yes
package_manager update -f
;;
brew)
package_manager upgrade
;;
esac
}
@@ -655,11 +657,6 @@ install_packages() {
brew)
package_manager install \
--force \
--formula \
"$@"
package_manager link \
--force \
--overwrite \
"$@"
;;
apk)
@@ -682,7 +679,7 @@ install_brew() {
print "Installing Homebrew..."
bash="$(require bash)"
script=$(download_file "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh")
script=$(download_file "https://github.com/Homebrew/install/raw/main/install.sh")
execute_as_user "$bash" -lc "NONINTERACTIVE=1 $script"
case "$arch" in
@@ -766,6 +763,12 @@ install_common_software() {
editors/vim \
sysutils/neofetch \
;;
brew)
# https://brew.sh
install_packages \
coreutils \
neofetch \
;;
esac
case "$distro" in
@@ -1163,7 +1166,8 @@ install_llvm() {
install_packages "llvm-$(llvm_version)-tools"
;;
brew)
install_packages "llvm@$(llvm_version)"
install_packages "llvm@$(llvm_version)" "lld@$(llvm_version)"
package_manager link "llvm@$(llvm_version)" "lld@$(llvm_version)"
;;
apk)
install_packages \
@@ -1184,7 +1188,13 @@ install_llvm() {
}
install_gcc() {
if ! [ "$os" = "linux" ] || ! [ "$distro" = "ubuntu" ] || [ -z "$gcc_version" ]; then
if ! [ "$os" = "linux" ]; then
return
fi
if ! [ "$distro" = "ubuntu" ]; then
return
fi
if [ -z "$gcc_version" ]; then
return
fi
@@ -1260,7 +1270,7 @@ install_sccache() {
case "$os" in
linux)
;;
freebsd)
freebsd | darwin)
cargo install sccache --locked --version 0.12.0
return
;;
@@ -1307,6 +1317,9 @@ install_rust() {
create_directory "$HOME/.cargo/bin"
append_to_path "$HOME/.cargo/bin"
;;
macos)
install_packages rust
;;
*)
rust_home="/opt/rust"
create_directory "$rust_home"
@@ -1333,7 +1346,7 @@ install_docker() {
case "$pm" in
brew)
if ! [ -d "/Applications/Docker.app" ]; then
package_manager install docker --cask
install_packages docker docker-compose
fi
;;
pkg)
@@ -1358,6 +1371,10 @@ install_docker() {
;;
esac
if [ "$os" = "darwin" ]; then
return
fi
systemctl="$(which systemctl)"
if [ -f "$systemctl" ]; then
execute_sudo "$systemctl" enable docker
@@ -1385,7 +1402,10 @@ macos_sdk_version() {
}
install_osxcross() {
if ! [ "$os" = "linux" ] || ! [ "$osxcross" = "1" ]; then
if ! [ "$os" = "linux" ]; then
return
fi
if ! [ "$osxcross" = "1" ]; then
return
fi
@@ -1431,9 +1451,7 @@ install_tailscale() {
execute "$sh" "$tailscale_script"
;;
darwin)
install_packages go
execute_as_user go install tailscale.com/cmd/tailscale{,d}@latest
append_to_path "$home/go/bin"
install_packages tailscale
;;
freebsd)
install_packages security/tailscale
@@ -1666,6 +1684,9 @@ install_chromium() {
install_packages \
www/chromium \
;;
brew)
install_packages --cask google-chrome
;;
esac
case "$distro" in
@@ -1705,6 +1726,17 @@ install_age() {
;;
esac
;;
darwin)
case "$arch" in
aarch64)
age_arch="arm64"
age_hash="cf79875bd5970dc2dac60c87fa50cee1ff1f9a41b0eb273f65e174aff37c367a"
;;
*)
error "Unsupported platform: $os-$arch"
;;
esac
;;
*)
error "Unsupported platform: $os-$arch"
;;
@@ -1753,6 +1785,9 @@ clean_system() {
if ! [ "$ci" = "1" ]; then
return
fi
if [ "$os" = "darwin" ]; then
return
fi
print "Cleaning system..."

View File

@@ -31,12 +31,12 @@ import {
sha256,
spawn,
spawnSafe,
spawnScp,
spawnSsh,
spawnSshSafe,
spawnSyncSafe,
startGroup,
tmpdir,
waitForPort,
which,
writeFile,
} from "./utils.mjs";
@@ -943,64 +943,6 @@ async function getGithubOrgSshKeys(organization) {
* @property {number} [retries]
*/
/**
* @typedef ScpOptions
* @property {string} hostname
* @property {string} source
* @property {string} destination
* @property {string[]} [identityPaths]
* @property {string} [port]
* @property {string} [username]
* @property {number} [retries]
*/
/**
* @param {ScpOptions} options
* @returns {Promise<void>}
*/
async function spawnScp(options) {
const { hostname, port, username, identityPaths, password, source, destination, retries = 3 } = options;
await waitForPort({ hostname, port: port || 22 });
const command = ["scp", "-o", "StrictHostKeyChecking=no"];
command.push("-O"); // use SCP instead of SFTP
if (!password) {
command.push("-o", "BatchMode=yes");
}
if (port) {
command.push("-P", port);
}
if (password) {
const sshPass = which("sshpass", { required: true });
command.unshift(sshPass, "-p", password);
} else if (identityPaths) {
command.push(...identityPaths.flatMap(path => ["-i", path]));
}
command.push(resolve(source));
if (username) {
command.push(`${username}@${hostname}:${destination}`);
} else {
command.push(`${hostname}:${destination}`);
}
let cause;
for (let i = 0; i < retries; i++) {
const result = await spawn(command, { stdio: "inherit" });
const { exitCode, stderr } = result;
if (exitCode === 0) {
return;
}
cause = stderr.trim() || undefined;
if (/(bad configuration option)|(no such file or directory)/i.test(stderr)) {
break;
}
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
throw new Error(`SCP failed: ${source} -> ${username}@${hostname}:${destination}`, { cause });
}
/**
* @param {string} passwordData
* @param {string} privateKeyPath

View File

@@ -1,5 +1,5 @@
import { inspect } from "node:util";
import { isPrivileged, spawnSafe, which } from "./utils.mjs";
import { isPrivileged, spawnSafe, spawnScp, spawnSshSafe, which } from "./utils.mjs";
/**
* @link https://tart.run/
@@ -48,6 +48,7 @@ export const tart = {
throw new Error(`Unsupported platform: ${inspect(platform)}`);
}
const distros = {
"26": "tahoe",
"15": "sequoia",
"14": "sonoma",
"13": "ventura",
@@ -83,14 +84,14 @@ export const tart = {
* @returns {Promise<TartVm | undefined>}
*/
async getVm(name) {
const result = await this.spawn(["get", name], {
json: true,
throwOnError: error => !/does not exist/i.test(inspect(error)),
return new Promise(async resolve => {
const result = await this.spawn(["get", name], {
json: true,
throwOnError: error => !/does not exist/i.test(inspect(error)),
});
if (!result) resolve(undefined);
else resolve({ Name: name, ...result });
});
return {
Name: name,
...result,
};
},
/**
@@ -265,6 +266,15 @@ export const tart = {
await spawnRdp({ ...connectOptions });
};
const snapshot = async tag => {
// https://tart.run/integrations/vm-management/#working-with-a-remote-oci-container-registry
// https://tart.run/integrations/vm-management/#pushing-a-local-image
// https://tart.run/integrations/buildkite/
// tart push ${name} ghcr.io/oven-sh/macos-${major_version}-tart-base:${tag}
// TODO: push to ghcr and return that full remote_image_name:tag
return tag;
};
const close = async () => {
await this.deleteVm(name);
};
@@ -274,8 +284,10 @@ export const tart = {
id: name,
spawn: exec,
spawnSafe: execSafe,
rdp,
attach,
upload,
snapshot,
close,
[Symbol.asyncDispose]: close,
};

View File

@@ -124,6 +124,7 @@ export function setEnv(name, value) {
* @property {(error: Error) => boolean} [retryOnError]
* @property {string} [stdin]
* @property {boolean} [privileged]
* @property {boolean} [detached]
*/
/**
@@ -2186,7 +2187,7 @@ export async function getCloudMetadata(name, cloud) {
throw new Error(`Unsupported cloud: ${inspect(cloud)}`);
}
const { error, body } = await curl(url, { headers, retries: 10 });
const { error, body } = await curl(url, { headers, retries: 3 });
if (error) {
console.warn("Failed to get cloud metadata:", error);
return;
@@ -2237,7 +2238,7 @@ export async function getBuildMetadata(name) {
* @returns {Promise<Error | undefined>}
*/
export async function waitForPort(options) {
const { hostname, port, retries = 10 } = options;
const { hostname, port, retries = 3 } = options;
console.log("Connecting...", `${hostname}:${port}`);
let cause;
@@ -2247,7 +2248,7 @@ export async function waitForPort(options) {
}
const connected = new Promise((resolve, reject) => {
const socket = connect({ host: hostname, port });
const socket = connect({ host: hostname, port, timeout: 10_000 });
socket.on("connect", () => {
socket.destroy();
console.log("Connected:", `${hostname}:${port}`);
@@ -3025,7 +3026,7 @@ export async function spawnSshSafe(options, spawnOptions = {}) {
* @returns {Promise<import("./utils.mjs").SpawnResult>}
*/
export async function spawnSsh(options, spawnOptions = {}) {
const { hostname, port, username, identityPaths, password, retries = 10, command: spawnCommand } = options;
const { hostname, port, username, identityPaths, password, retries = 3, command: spawnCommand } = options;
if (!hostname.includes("@")) {
await waitForPort({
@@ -3114,3 +3115,61 @@ export async function setupUserData(machine, options) {
rm(tmpFile);
}
}
/**
* @typedef ScpOptions
* @property {string} hostname
* @property {string} source
* @property {string} destination
* @property {string[]} [identityPaths]
* @property {string} [port]
* @property {string} [username]
* @property {number} [retries]
*/
/**
* @param {ScpOptions} options
* @returns {Promise<void>}
*/
export async function spawnScp(options) {
const { hostname, port, username, identityPaths, password, source, destination, retries = 3 } = options;
await waitForPort({ hostname, port: port || 22 });
const command = ["scp", "-o", "StrictHostKeyChecking=no"];
command.push("-O"); // use SCP instead of SFTP
if (!password) {
command.push("-o", "BatchMode=yes");
}
if (port) {
command.push("-P", port);
}
if (password) {
const sshPass = which("sshpass", { required: true });
command.unshift(sshPass, "-p", password);
} else if (identityPaths) {
command.push(...identityPaths.flatMap(path => ["-i", path]));
}
command.push(resolve(source));
if (username) {
command.push(`${username}@${hostname}:${destination}`);
} else {
command.push(`${hostname}:${destination}`);
}
let cause;
for (let i = 0; i < retries; i++) {
const result = await spawn(command, { stdio: "inherit" });
const { exitCode, stderr } = result;
if (exitCode === 0) {
return;
}
cause = stderr.trim() || undefined;
if (/(bad configuration option)|(no such file or directory)/i.test(stderr)) {
break;
}
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
throw new Error(`SCP failed: ${source} -> ${username}@${hostname}:${destination}`, { cause });
}