diff --git a/.nix/pkgs/graphite.nix b/.nix/pkgs/graphite.nix index 7fe041054d..f9bf9bc2b7 100644 --- a/.nix/pkgs/graphite.nix +++ b/.nix/pkgs/graphite.nix @@ -109,7 +109,7 @@ deps.crane.lib.buildPackage ( ''; preBuild = '' - # Prevent `package-installer.js` from trying to update npm dependencies + # Prevent `cargo-run`'s frontend setup from trying to update npm dependencies touch -r frontend/package-lock.json -d '+1 year' frontend/node_modules/.install-timestamp export HOME="$TMPDIR" diff --git a/frontend/branding-installer.js b/frontend/branding-installer.js deleted file mode 100644 index 640d0fa355..0000000000 --- a/frontend/branding-installer.js +++ /dev/null @@ -1,147 +0,0 @@ -/* eslint-disable no-console */ - -import crypto from "crypto"; -import fs from "fs"; -import http from "http"; -import https from "https"; -import path from "path"; -import { fileURLToPath } from "url"; -import zlib from "zlib"; -import * as tar from "tar"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const BRANDING_INFO_FILE = path.join(__dirname, "../.branding"); -const BRANDING_DIR = path.join(__dirname, "../branding"); -const INSTALLED_BRANDING_INFO_FILE = path.join(BRANDING_DIR, ".branding"); -const TEMP_FILE = path.join(__dirname, "branding_download.tar.gz"); - -function downloadFile(url, dest) { - return new Promise((resolve, reject) => { - const file = fs.createWriteStream(dest); - const protocol = url.startsWith("https") ? https : http; - - const request = protocol.get(url, (response) => { - if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307) { - file.close(); - fs.unlink(dest, () => {}); - if (response.headers.location) { - downloadFile(response.headers.location, dest).then(resolve).catch(reject); - } else { - reject(new Error("Redirect location missing")); - } - return; - } - - if (response.statusCode !== 200) { - file.close(); - fs.unlink(dest, () => {}); - reject(new Error(`Failed to download: ${response.statusCode}`)); - return; - } - - response.pipe(file); - file.on("finish", () => { - file.close(resolve); - }); - }); - - request.on("error", (err) => { - fs.unlink(dest, () => {}); - reject(err); - }); - }); -} - -async function main() { - if (!fs.existsSync(BRANDING_INFO_FILE)) { - console.error(`Branding info file not found at ${BRANDING_INFO_FILE}`); - process.exit(1); - } - - const content = fs.readFileSync(BRANDING_INFO_FILE, "utf8"); - - if (fs.existsSync(INSTALLED_BRANDING_INFO_FILE)) { - const installedContent = fs.readFileSync(INSTALLED_BRANDING_INFO_FILE, "utf8"); - if (content === installedContent) { - console.log("Branding assets are up to date."); - return; - } - } - - const lines = content - .split("\n") - .map((l) => l.trim()) - .filter((l) => l.length > 0); - - if (lines.length < 2) { - console.error("Branding file must contain at least two lines: URL and Hash"); - process.exit(1); - } - - const url = lines[0]; - const expectedHash = lines[1]; - - console.log(`Downloading branding assets from <${url}>...`); - - try { - await downloadFile(url, TEMP_FILE); - } catch (err) { - console.error("Download failed:", err); - process.exit(1); - } - - console.log("Download complete. Verifying hash..."); - - const fileBuffer = fs.readFileSync(TEMP_FILE); - const hashSum = crypto.createHash("sha256"); - hashSum.update(fileBuffer); - const hex = hashSum.digest("hex"); - - if (hex !== expectedHash) { - console.error("Hash mismatch!"); - console.error(`Expected: ${expectedHash}`); - console.error(`Actual: ${hex}`); - if (fs.existsSync(TEMP_FILE)) fs.unlinkSync(TEMP_FILE); - process.exit(1); - } - - console.log("Hash verified. Extracting..."); - - if (fs.existsSync(BRANDING_DIR)) { - fs.rmSync(BRANDING_DIR, { recursive: true, force: true }); - } - fs.mkdirSync(BRANDING_DIR, { recursive: true }); - - try { - // Extract the tar.gz file - await new Promise((resolve, reject) => { - fs.createReadStream(TEMP_FILE) - .pipe(zlib.createGunzip()) - .pipe( - tar.x({ - cwd: BRANDING_DIR, - strip: 1, - }), - ) - .on("error", reject) - .on("finish", resolve); - }); - fs.copyFileSync(BRANDING_INFO_FILE, INSTALLED_BRANDING_INFO_FILE); - console.log("Extraction complete."); - } catch (error) { - console.error("Failed to extract archive:", error); - } finally { - if (fs.existsSync(TEMP_FILE)) { - fs.unlinkSync(TEMP_FILE); - } - } -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error("An error occurred:", err); - process.exit(1); - }); diff --git a/frontend/package-installer.js b/frontend/package-installer.js deleted file mode 100644 index 71ee540314..0000000000 --- a/frontend/package-installer.js +++ /dev/null @@ -1,43 +0,0 @@ -// This script automatically installs the npm packages listed in package-lock.json and runs as part of `npm run setup` (invoked by `cargo run`). -// It skips the installation if this has already run and neither package.json nor package-lock.json has been modified since. - -import { execSync } from "child_process"; -import { existsSync, statSync, writeFileSync } from "fs"; - -const INSTALL_TIMESTAMP_FILE = "node_modules/.install-timestamp"; - -// Checks if the install is needed by comparing modification times -const isInstallNeeded = () => { - if (!existsSync(INSTALL_TIMESTAMP_FILE)) return true; - - const timestamp = statSync(INSTALL_TIMESTAMP_FILE).mtime; - return ["package.json", "package-lock.json", "package-installer.js"].some((file) => { - return existsSync(file) && statSync(file).mtime > timestamp; - }); -}; - -// Run `npm ci` if needed and update the install timestamp -if (isInstallNeeded()) { - try { - // eslint-disable-next-line no-console - console.log("Installing npm packages..."); - - // Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks. - // The devDependencies are explicitly included because they hold the build tooling (Vite, etc.), which npm would - // otherwise omit in environments that set NODE_ENV=production (like CI does for the sake of the Vite build). - execSync("npm ci --include=dev --prefer-offline --no-audit --no-fund", { stdio: "inherit" }); - - // Touch the install timestamp file - writeFileSync(INSTALL_TIMESTAMP_FILE, ""); - - // eslint-disable-next-line no-console - console.log("Finished installing npm packages."); - } catch (_) { - // eslint-disable-next-line no-console - console.error("\n\n--------------------> Failed to install npm packages. Please delete `/frontend/node_modules` then try again.\n\n"); - process.exit(1); - } -} else { - // eslint-disable-next-line no-console - console.log("All npm packages are up-to-date."); -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 045b7cd334..e5b3d4fe79 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,7 +26,6 @@ "sass": "^1.99.0", "svelte": "^5.55.1", "svelte-check": "^4.4.6", - "tar": "^7.5.13", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.3" @@ -293,19 +292,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2058,16 +2044,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4394,19 +4370,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -5919,23 +5882,6 @@ "url": "https://opencollective.com/synckit" } }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6523,16 +6469,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a02e6afb9f..0c01ff212e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,7 @@ "type": "module", "scripts": { "check": "svelte-check --fail-on-warnings && eslint", - "fix": "eslint --fix", - "setup": "node package-installer.js && node branding-installer.js" + "fix": "eslint --fix" }, "//": "NOTE: `source-sans-pro` is never to be upgraded to 3.x because that renders 1px above its intended position.", "///": "Waiting on before we can update @eslint/js and eslint to 10.x", @@ -32,7 +31,6 @@ "sass": "^1.99.0", "svelte": "^5.55.1", "svelte-check": "^4.4.6", - "tar": "^7.5.13", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.3" diff --git a/tools/cargo-run/internal/download/src/main.rs b/tools/cargo-run/internal/download/src/main.rs index eb04ee2b46..bdbfaf6cc7 100644 --- a/tools/cargo-run/internal/download/src/main.rs +++ b/tools/cargo-run/internal/download/src/main.rs @@ -106,7 +106,6 @@ fn extract_tar_gz(body: &[u8], dir: &Path, strip: usize, include: &[String]) -> } entry.unpack(&target).map_err(|e| Error::Io(e, format!("unpacking '{}'", target.display())))?; } - eprintln!("Extracted into {}", dir.display()); Ok(()) } diff --git a/tools/cargo-run/src/branding.rs b/tools/cargo-run/src/branding.rs new file mode 100644 index 0000000000..2c822c3925 --- /dev/null +++ b/tools/cargo-run/src/branding.rs @@ -0,0 +1,40 @@ +use crate::cmd::prelude::*; +use crate::{Error, workspace_dir}; + +const INFO_FILE: &str = ".branding"; +const DIR: &str = "branding"; + +pub fn setup() -> Result<(), Error> { + let workspace = workspace_dir(); + let info_path = workspace.join(INFO_FILE); + let dir_path = workspace.join(DIR); + let marker_path = dir_path.join(INFO_FILE); + + let info = std::fs::read_to_string(&info_path).map_err(|e| Error::Io(e, format!("reading '{}'", info_path.display())))?; + + if let Ok(marker) = std::fs::read_to_string(&marker_path) + && marker == info + { + return Ok(()); + } + + let mut lines = info.lines().map(str::trim).filter(|l| !l.is_empty()); + let url = lines + .next() + .ok_or_else(|| Error::Io(std::io::Error::other("missing URL"), format!("parsing '{}'", info_path.display())))?; + let sha256 = lines + .next() + .ok_or_else(|| Error::Io(std::io::Error::other("missing SHA-256"), format!("parsing '{}'", info_path.display())))?; + + eprintln!("Downloading branding assets from <{url}>..."); + + if dir_path.exists() { + std::fs::remove_dir_all(&dir_path).map_err(|e| Error::Io(e, format!("removing '{}'", dir_path.display())))?; + } + + utils::internal("download").args([url, sha256, DIR, "--extract", "--strip", "1"]).dir(&workspace).run()?; + + std::fs::copy(&info_path, &marker_path).map_err(|e| Error::Io(e, format!("writing '{}'", marker_path.display())))?; + + Ok(()) +} diff --git a/tools/cargo-run/src/frontend.rs b/tools/cargo-run/src/frontend.rs index 9648c6f9b7..c165ef13e0 100644 --- a/tools/cargo-run/src/frontend.rs +++ b/tools/cargo-run/src/frontend.rs @@ -11,7 +11,32 @@ pub fn frontend_dir() -> PathBuf { } pub fn setup() -> Result<(), Error> { - utils::npm(["run", "setup"]).dir(frontend_dir()).run() + let frontend = frontend_dir(); + let node_modules = frontend.join("node_modules"); + let timestamp_path = node_modules.join(".install-timestamp"); + + let mtime = |p: PathBuf| std::fs::metadata(p).and_then(|m| m.modified()).ok(); + + if let Some(install_time) = mtime(timestamp_path.clone()) + && let Some(package_json_time) = mtime(frontend.join("package.json")) + && let Some(package_lock_json_time) = mtime(frontend.join("package-lock.json")) + && install_time >= package_json_time + && install_time >= package_lock_json_time + { + return Ok(()); + } + + eprintln!("Installing npm packages..."); + let install = || utils::npm(["ci", "--include=dev", "--prefer-offline", "--no-audit", "--no-fund"]).dir(&frontend).run(); + if install().is_err() { + eprintln!("Failed to install npm packages. Wiping `frontend/node_modules` and retrying..."); + let _ = std::fs::remove_dir_all(&node_modules); + install()?; + } + + std::fs::write(×tamp_path, "").map_err(|e| Error::Io(e, format!("writing '{}'", timestamp_path.display())))?; + eprintln!("Finished installing npm packages."); + Ok(()) } pub fn build_wasm(release: bool, native: bool) -> Result<(), Error> { diff --git a/tools/cargo-run/src/lib.rs b/tools/cargo-run/src/lib.rs index 174c810592..261aa745c2 100644 --- a/tools/cargo-run/src/lib.rs +++ b/tools/cargo-run/src/lib.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +pub mod branding; pub mod cmd; pub mod frontend; pub mod requirements; diff --git a/tools/cargo-run/src/main.rs b/tools/cargo-run/src/main.rs index 96e5cdc110..8928ea56cf 100644 --- a/tools/cargo-run/src/main.rs +++ b/tools/cargo-run/src/main.rs @@ -87,6 +87,10 @@ fn run_task(task: &Task) -> Result<(), Error> { requirements::check(task)?; + if !matches!(task.target, Target::Cli) { + branding::setup()?; + } + match (&task.action, &task.target, &task.profile) { (Action::Run, Target::Web, Profile::Debug | Profile::Default) => frontend::watch(false)?, (Action::Run, Target::Web, Profile::Release) => frontend::watch(true)?, diff --git a/tools/cargo-run/src/requirements.rs b/tools/cargo-run/src/requirements.rs index ec690b8967..4a4aa4c29e 100644 --- a/tools/cargo-run/src/requirements.rs +++ b/tools/cargo-run/src/requirements.rs @@ -29,7 +29,7 @@ fn requirements(task: &Task) -> Vec { command: "rustc", args: &["--print", "target-libdir", "--target", "wasm32-unknown-unknown"], check: Check::Matches(&|out| std::path::Path::new(out.trim()).is_dir()), - name: "Rust (Wasm Target)", + name: "Rust - Wasm Target", install: "rustup target add wasm32-unknown-unknown".into(), skip: Some(&|task| matches!(task.target, Target::Cli)), ..Default::default()