From b7f668c58a760f8187daa55c0aeaeba0b8584121 Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 7 Aug 2025 22:59:51 -0500 Subject: [PATCH] feat: revamp web build script in bun + typescript, delete old scripts --- build.sh | 74 --------------- build.ts | 201 ---------------------------------------- web.build.ts | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 275 deletions(-) delete mode 100755 build.sh delete mode 100644 build.ts create mode 100644 web.build.ts diff --git a/build.sh b/build.sh deleted file mode 100755 index 7b7a29e..0000000 --- a/build.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -set -eu - -release='false' -serve='false' -skip_emsdk='false' -clean='false' - -print_usage() { - printf "Usage: -erdsc\n" - printf " -e: Skip EMSDK setup (GitHub workflow only)\n" - printf " -r: Build in release mode\n" - printf " -d: Build in debug mode\n" - printf " -s: Serve the WASM files once built\n" - printf " -c: Clean the target/dist directory\n" -} - -while getopts 'erdsc' flag; do - case "${flag}" in - e) skip_emsdk='true' ;; - r) release='true' ;; - d) release='false' ;; # doesn't actually do anything, but last flag wins - s) serve='true' ;; - c) clean='true' ;; - *) - print_usage - exit 1 - ;; - esac -done - -if [ "$clean" = 'true' ]; then - echo "Cleaning target directory" - cargo clean - rm -rf ./dist/ -fi - -if [ "$skip_emsdk" = 'false' ]; then - echo "Activating Emscripten" - # SDL2-TTF requires 3.1.43, fails to build on latest - ../emsdk/emsdk activate 3.1.43 - source ../emsdk/emsdk_env.sh -fi - -echo "Building WASM with Emscripten" -build_type='debug' -if [ "$release" = 'true' ]; then - cargo build --target=wasm32-unknown-emscripten --release - build_type='release' -else - cargo build --target=wasm32-unknown-emscripten -fi - -echo "Generating CSS" -pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css - -echo "Copying WASM files" -mkdir -p dist -output_folder="target/wasm32-unknown-emscripten/$build_type" - -cp assets/site/{build.css,favicon.ico,index.html} dist -cp $output_folder/pacman.{wasm,js} dist -if [ -f $output_folder/deps/pacman.data ]; then - cp $output_folder/deps/pacman.data dist -fi - -if [ -f $output_folder/pacman.wasm.map ]; then - cp $output_folder/pacman.wasm.map dist -fi - -if [ "$serve" = 'true' ]; then - echo "Serving WASM with Emscripten" - python3 -m http.server -d ./dist/ 8080 -fi diff --git a/build.ts b/build.ts deleted file mode 100644 index 85962ff..0000000 --- a/build.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { $ } from "bun"; - -// This is a bun script, run with `bun run build.ts` - -import * as path from "path"; -import * as fs from "fs/promises"; - -async function clean() { - console.log("Cleaning..."); - await $`cargo clean`; - await $`rm -rf ./dist/`; - console.log("Cleaned..."); -} - -async function setupEmscripten() { - const emsdkDir = "./emsdk"; - const emsdkExists = await fs - .access(emsdkDir) - .then(() => true) - .catch(() => false); - - if (!emsdkExists) { - console.log("Cloning Emscripten SDK..."); - await $`git clone https://github.com/emscripten-core/emsdk.git`; - } else { - console.log("Emscripten SDK already exists, skipping clone."); - } - - const emscriptenToolchainPath = path.join(emsdkDir, "upstream", "emscripten"); - const toolchainInstalled = await fs - .access(emscriptenToolchainPath) - .then(() => true) - .catch(() => false); - - if (!toolchainInstalled) { - console.log("Installing Emscripten toolchain..."); - await $`./emsdk/emsdk install 3.1.43`; - } else { - console.log( - "Emscripten toolchain 3.1.43 already installed, skipping install." - ); - } - - console.log("Activating Emscripten..."); - await $`./emsdk/emsdk activate 3.1.43`; - console.log("Emscripten activated."); - - // Set EMSDK environment variable for subsequent commands - process.env.EMSDK = path.resolve(emsdkDir); - - const emsdkPython = path.join(path.resolve(emsdkDir), "python"); - const emsdkNode = path.join(path.resolve(emsdkDir), "node", "16.20.0_64bit"); // Adjust node version if needed - const emsdkBin = path.join(path.resolve(emsdkDir), "upstream", "emscripten"); - process.env.PATH = `${emsdkPython}:${emsdkNode}:${emsdkBin}:${process.env.PATH}`; -} - -async function buildWeb(release: boolean) { - console.log("Building WASM with Emscripten..."); - const rustcFlags = [ - "-C", - "link-arg=--preload-file", - "-C", - "link-arg=assets", - ].join(" "); - - if (release) { - await $`env RUSTFLAGS=${rustcFlags} cargo build --target=wasm32-unknown-emscripten --release`; - } else { - await $`env RUSTFLAGS=${rustcFlags} cargo build --target=wasm32-unknown-emscripten`; - } - - console.log("Generating CSS..."); - await $`pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css`; - - console.log("Copying WASM files..."); - const buildType = release ? "release" : "debug"; - const outputFolder = `target/wasm32-unknown-emscripten/${buildType}`; - await $`mkdir -p dist`; - await $`cp assets/site/index.html dist`; - await $`cp assets/site/*.woff* dist`; - await $`cp assets/site/build.css dist`; - await $`cp assets/site/favicon.ico dist`; - await $`cp ${outputFolder}/pacman.wasm dist`; - await $`cp ${outputFolder}/pacman.js dist`; - - // Check if .data file exists before copying - try { - await fs.access(`${outputFolder}/pacman.data`); - await $`cp ${outputFolder}/pacman.data dist`; - } catch (e) { - console.log("No pacman.data file found, skipping copy."); - } - - // Check if .map file exists before copying - try { - await fs.access(`${outputFolder}/pacman.wasm.map`); - await $`cp ${outputFolder}/pacman.wasm.map dist`; - } catch (e) { - console.log("No pacman.wasm.map file found, skipping copy."); - } - - console.log("WASM files copied."); -} - -async function serve() { - console.log("Serving WASM with Emscripten..."); - await $`python3 -m http.server -d ./dist/ 8080`; -} - -async function main() { - const args = process.argv.slice(2); - - let release = false; - let serveFiles = false; - let skipEmscriptenSetup = false; - let cleanProject = false; - let target = "web"; // Default target - - for (const arg of args) { - switch (arg) { - case "-r": - release = true; - break; - case "-s": - serveFiles = true; - break; - case "-e": - skipEmscriptenSetup = true; - break; - case "-c": - cleanProject = true; - break; - case "--target=linux": - target = "linux"; - break; - case "--target=windows": - target = "windows"; - break; - case "--target=web": - target = "web"; - break; - case "-h": - case "--help": - console.log(` -Usage: ts-node build.ts [options] - -Options: - -r Build in release mode - -s Serve the WASM files once built (for web target) - -e Skip EMSDK setup (GitHub workflow only) - -c Clean the target/dist directory - --target=[web|linux|windows] Specify target platform (default: web) - -h, --help Show this help message - `); - return; - } - } - - if (cleanProject) { - await clean(); - } - - if (!skipEmscriptenSetup && target === "web") { - await setupEmscripten(); - } - - switch (target) { - case "web": - await buildWeb(release); - if (serveFiles) { - await serve(); - } - break; - case "linux": - console.log("Building for Linux..."); - if (release) { - await $`cargo build --release`; - } else { - await $`cargo build`; - } - console.log("Linux build complete."); - break; - case "windows": - console.log("Building for Windows..."); - if (release) { - await $`cargo build --release --target=x86_64-pc-windows-msvc`; // Assuming MSVC toolchain - } else { - await $`cargo build --target=x86_64-pc-windows-msvc`; - } - console.log("Windows build complete."); - break; - default: - console.error("Invalid target specified."); - process.exit(1); - } -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/web.build.ts b/web.build.ts new file mode 100644 index 0000000..0cf0702 --- /dev/null +++ b/web.build.ts @@ -0,0 +1,252 @@ +import { $ } from "bun"; +import { existsSync, promises as fs } from "fs"; +import { platform } from "os"; +import { dirname, join, relative, resolve } from "path"; +import { match, P } from "ts-pattern"; + +type Os = + | { type: "linux"; wsl: boolean } + | { type: "windows" } + | { type: "macos" }; + +const os: Os = match(platform()) + .with("win32", () => ({ type: "windows" as const })) + .with("linux", () => ({ + type: "linux" as const, + // We detect WSL by checking for the presence of the WSLInterop file. + // This is a semi-standard method of detecting WSL, which is more than workable for this already hacky script. + wsl: existsSync("/proc/sys/fs/binfmt_misc/WSLInterop"), + })) + .with("darwin", () => ({ type: "macos" as const })) + .otherwise(() => { + throw new Error(`Unsupported platform: ${platform()}`); + }); + +function log(msg: string) { + console.log(`[web.build] ${msg}`); +} + +/** + * Build the application with Emscripten, generate the CSS, and copy the files into 'dist'. + * + * @param release - Whether to build in release mode. + * @param env - The environment variables to inject into build commands. + */ +async function build(release: boolean, env: Record) { + log( + `Building for 'wasm32-unknown-emscripten' for ${ + release ? "release" : "debug" + }` + ); + await $`cargo build --target=wasm32-unknown-emscripten ${ + release ? "--release" : "" + }`.env(env); + + log("Generating CSS"); + await $`pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css`; + + const buildType = release ? "release" : "debug"; + const siteFolder = resolve("assets/site"); + const outputFolder = resolve(`target/wasm32-unknown-emscripten/${buildType}`); + const dist = resolve("dist"); + + // The files to copy into 'dist' + const files = [ + ...["index.html", "favicon.ico", "build.css"].map((file) => ({ + src: join(siteFolder, file), + dest: join(dist, file), + optional: false, + })), + ...["pacman.wasm", "pacman.js", "deps/pacman.data"].map((file) => ({ + src: join(outputFolder, file), + dest: join(dist, file.split("/").pop() || file), + optional: false, + })), + { + src: join(outputFolder, "pacman.wasm.map"), + dest: join(dist, "pacman.wasm.map"), + optional: true, + }, + ]; + + // Create required destination folders + await Promise.all( + // Get the dirname of files, remove duplicates + [...new Set(files.map(({ dest }) => dirname(dest)))] + // Create the folders + .map(async (dir) => { + // If the folder doesn't exist, create it + if (!(await fs.exists(dir))) { + log(`Creating folder ${dir}`); + await fs.mkdir(dir, { recursive: true }); + } + }) + ); + + // Copy the files to the dist folder + log("Copying files into dist"); + await Promise.all( + files.map(async ({ optional, src, dest }) => { + match({ optional, exists: await fs.exists(src) }) + // If optional and doesn't exist, skip + .with({ optional: true, exists: false }, () => { + log( + `Optional file ${os.type === "windows" ? "\\" : "/"}${relative( + process.cwd(), + src + )} does not exist, skipping...` + ); + }) + // If not optional and doesn't exist, throw an error + .with({ optional: false, exists: false }, () => { + throw new Error(`Required file ${src} does not exist`); + }) + // Otherwise, copy the file + .otherwise(async () => await fs.copyFile(src, dest)); + }) + ); +} + +/** + * Checks to see if the Emscripten SDK is activated for a Windows or *nix machine by looking for a .exe file and the equivalent file on Linux/macOS. Returns both results for handling. + * @param emsdkDir - The directory containing the Emscripten SDK. + * @returns A record of environment variables. + */ +async function checkEmsdkType( + emsdkDir: string +): Promise<{ windows: boolean; nix: boolean }> { + const binary = resolve(join(emsdkDir, "upstream", "bin", "clang")); + + return { + windows: await fs.exists(binary + ".exe"), + nix: await fs.exists(binary), + }; +} + +/** + * Activate the Emscripten SDK environment variables. + * Technically, this doesn't actaully activate the environment variables for the current shell, + * it just runs the environment sourcing script and returns the environment variables for future command invocations. + * @param emsdkDir - The directory containing the Emscripten SDK. + * @returns A record of environment variables. + */ +async function activateEmsdk( + emsdkDir: string +): Promise<{ vars: Record } | { err: string }> { + // Determine the environment script to use based on the OS + const envScript = match(os) + .with({ type: "windows" }, () => join(emsdkDir, "emsdk_env.bat")) + .with({ type: P.union("linux", "macos") }, () => + join(emsdkDir, "emsdk_env.sh") + ) + .exhaustive(); + + // Run the environment script and capture the output + const { stdout, stderr, exitCode } = await match(os) + .with({ type: "windows" }, () => + // run the script, ignore it's output ('>nul'), then print the environment variables ('set') + $`cmd /c "${envScript} >nul && set"`.quiet() + ) + .with({ type: P.union("linux", "macos") }, () => + // run the script with bash, ignore it's output ('> /dev/null'), then print the environment variables ('env') + $`bash -c "source '${envScript}' && env"`.quiet() + ) + .exhaustive(); + + if (exitCode !== 0) { + return { err: stderr.toString() }; + } + + // Parse the output into a record of environment variables + const vars = Object.fromEntries( + stdout + .toString() + .split(os.type === "windows" ? /\r?\n/ : "\n") // Split output into lines, handling Windows CRLF vs *nix LF + .map((line) => line.split("=", 2)) // Parse each line as KEY=VALUE (limit to 2 parts) + .filter(([k, v]) => k && v) // Keep only valid key-value pairs (both parts exist) + ); + + return { vars }; +} + +async function main() { + // Print the OS detected + log( + "OS Detected: " + + match(os) + .with({ type: "windows" }, () => "Windows") + .with({ type: "linux" }, ({ wsl: isWsl }) => + isWsl ? "Linux (via WSL)" : "Linux" + ) + .with({ type: "macos" }, () => "macOS") + .exhaustive() + ); + + const release = process.env.RELEASE !== "0"; + const emsdkDir = resolve("./emsdk"); + const vars = match(await activateEmsdk(emsdkDir)) // result handling + .with({ vars: P.select() }, (vars) => vars) + .with({ err: P.any }, ({ err }) => { + log("Error activating Emscripten SDK: " + err); + process.exit(1); + }) + .exhaustive(); + + // Check if the Emscripten SDK is activated/installed properly for the current OS + match({ + os: os, + ...(await checkEmsdkType(emsdkDir)), + }) + // If the Emscripten SDK is not activated/installed properly, exit with an error + .with( + { + nix: false, + windows: false, + }, + () => { + log( + "Emscripten SDK does not appear to be activated/installed properly." + ); + process.exit(1); + } + ) + // If the Emscripten SDK is activated for Windows, but is currently running on a *nix OS, exit with an error + .with( + { + nix: false, + windows: true, + os: { type: P.not("windows") }, + }, + () => { + log( + "Emscripten SDK appears to be activated for Windows, but is currently running on a *nix OS." + ); + process.exit(1); + } + ) + // If the Emscripten SDK is activated for *nix, but is currently running on a Windows OS, exit with an error + .with( + { + nix: true, + windows: false, + os: { type: "windows" }, + }, + () => { + log( + "Emscripten SDK appears to be activated for *nix, but is currently running on a Windows OS." + ); + process.exit(1); + } + ); + + // Build the application + await build(release, vars); +} + +/** + * Main entry point. + */ +main().catch((err) => { + console.error("[web.build] Error:", err); + process.exit(1); +});