mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 03:15:48 -06:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c39fcaa7d7 | |||
| 1d9499c4f8 | |||
| 61050a5585 | |||
| 85420711df | |||
| 2efa7a4df5 |
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -120,7 +120,7 @@ jobs:
|
|||||||
echo "Build attempt $attempt of $MAX_RETRIES"
|
echo "Build attempt $attempt of $MAX_RETRIES"
|
||||||
|
|
||||||
# Capture output and check for specific error while preserving real-time output
|
# Capture output and check for specific error while preserving real-time output
|
||||||
if bun run web.build.ts 2>&1 | tee /tmp/build_output.log; then
|
if bun run -i web.build.ts 2>&1 | tee /tmp/build_output.log; then
|
||||||
echo "Build successful on attempt $attempt"
|
echo "Build successful on attempt $attempt"
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ emsdk/
|
|||||||
.idea
|
.idea
|
||||||
rust-sdl2-emscripten/
|
rust-sdl2-emscripten/
|
||||||
assets/site/build.css
|
assets/site/build.css
|
||||||
|
tailwindcss-*
|
||||||
|
|||||||
47
README.md
47
README.md
@@ -14,8 +14,6 @@
|
|||||||
[demo]: https://xevion.github.io/Pac-Man/
|
[demo]: https://xevion.github.io/Pac-Man/
|
||||||
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
|
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
|
||||||
|
|
||||||
The game includes all the original features you'd expect from Pac-Man:
|
The game includes all the original features you'd expect from Pac-Man:
|
||||||
@@ -27,38 +25,63 @@ The game includes all the original features you'd expect from Pac-Man:
|
|||||||
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
|
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
|
||||||
- [x] Authentic sound effects and sprites
|
- [x] Authentic sound effects and sprites
|
||||||
|
|
||||||
Built with SDL2 for cross-platform graphics and audio, this implementation can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
|
This cross-platform implementation is built with SDL2 for graphics, audio, and input handling. It can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
|
||||||
|
|
||||||
## Feature Targets
|
## Why?
|
||||||
|
|
||||||
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors.
|
Just because. And because I wanted to learn more about Rust, inter-operability with C, and compiling to WebAssembly.
|
||||||
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly.
|
|
||||||
|
I was inspired by a certain code review video on YouTube; [SOME UNIQUE C++ CODE // Pacman Clone Code Review](https://www.youtube.com/watch?v=OKs_JewEeOo) by The Cherno.
|
||||||
|
|
||||||
|
For some reason, I was inspired to try and replicate it in Rust, and it was uniquely challenging.
|
||||||
|
|
||||||
|
I wanted to hit a log of goals and features, making it a 'perfect' project that I could be proud of.
|
||||||
|
|
||||||
|
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors. No hacks, workarounds, or poor designs.
|
||||||
|
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly. Statically linked, no runtime dependencies.
|
||||||
|
- Performant, low memory, CPU and GPU usage.
|
||||||
- Online demo, playable in a browser.
|
- Online demo, playable in a browser.
|
||||||
- Automatic build system, with releases for Windows, Linux, and Mac & Web-Assembly.
|
- Completely automatic build system with releases for all platforms.
|
||||||
|
- Well documented, well-tested, and maintainable.
|
||||||
|
|
||||||
|
## Experimental Ideas
|
||||||
|
|
||||||
- Debug tooling
|
- Debug tooling
|
||||||
- Game state visualization
|
- Game state visualization
|
||||||
- Game speed controls + pausing
|
- Game speed controls + pausing
|
||||||
- Log tracing
|
- Log tracing
|
||||||
- Performance details
|
- Performance details
|
||||||
|
- Customized Themes & Colors
|
||||||
## Experimental Ideas
|
- Color-blind friendly
|
||||||
|
|
||||||
- Perfected Ghost Algorithms
|
- Perfected Ghost Algorithms
|
||||||
- More than 4 ghosts
|
- More than 4 ghosts
|
||||||
- Custom Level Generation
|
- Custom Level Generation
|
||||||
- Multi-map tunnelling
|
- Multi-map tunnelling
|
||||||
- Online Scoreboard
|
- Online Scoreboard
|
||||||
- WebAssembly build contains a special API key for communicating with server.
|
- An online axum server with a simple database and OAuth2 authentication.
|
||||||
- To prevent abuse, the server will only accept scores from the WebAssembly build.
|
- Integrates with GitHub, Discord, and Google OAuth2 to acquire an email identifier & avatar.
|
||||||
|
- Avatars are optional for score submission and can be disabled, instead using a blank avatar.
|
||||||
|
- Avatars are downscaled to a low resolution pixellated image to maintain the 8-bit aesthetic.
|
||||||
|
- A custom name is used for the score submission, which is checked for potential abusive language.
|
||||||
|
- A max length of 14 characters, and a min length of 3 characters.
|
||||||
|
- Names are checked for potential abusive language via an external API.
|
||||||
|
- The client implementation should require zero configuration, environment variables, or special secrets.
|
||||||
|
- It simply defaults to the pacman server API, or can be overriden manually.
|
||||||
|
|
||||||
## Build Notes
|
## Build Notes
|
||||||
|
|
||||||
|
Since this project is still in progress, I'm only going to cover non-obvious build details. By reading the code, build scripts, and copying the online build workflows, you should be able to replicate the build process.
|
||||||
|
|
||||||
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg.
|
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg.
|
||||||
- For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git`
|
- For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git`
|
||||||
- The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead.
|
- The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead.
|
||||||
|
- I'm still not sure _why_ 3.1.43 is required, but it is. Perhaps in the future I will attempt to use a more modern version.
|
||||||
|
- Occasionally, the build will fail due to dependencies failing to download. I even have a retry mechanism in the build workflow due to this.
|
||||||
- You can then activate the Emscripten SDK with `source ./emsdk/emsdk_env.sh` or `./emsdk/emsdk_env.ps1` or `./emsdk/emsdk_env.bat` depending on your OS/terminal.
|
- You can then activate the Emscripten SDK with `source ./emsdk/emsdk_env.sh` or `./emsdk/emsdk_env.ps1` or `./emsdk/emsdk_env.bat` depending on your OS/terminal.
|
||||||
- While using the `web.build.ts` is not technically required, it simplifies the build process and is very helpful.
|
- While using the `web.build.ts` is not technically required, it simplifies the build process and is very helpful.
|
||||||
- It is intended to be run with `bun`, which you can acquire at [bun.sh](https://bun.sh/)
|
- It is intended to be run with `bun`, which you can acquire at [bun.sh](https://bun.sh/)
|
||||||
- Tip: You can launch a fileserver with `python` or `caddy` to serve the files in the `dist` folder.
|
- Tip: You can launch a fileserver with `python` or `caddy` to serve the files in the `dist` folder.
|
||||||
- `python3 -m http.server 8080 -d dist`
|
- `python3 -m http.server 8080 -d dist`
|
||||||
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))
|
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))
|
||||||
|
- `web.build.ts` auto installs dependencies, but you may need to pass `-i` or `--install=fallback|force` to install missing packages. My guess is that if you have some packages installed, it won't install any missing ones. If you have no packages installed, it will install all of them.
|
||||||
|
- If you want to have TypeScript resolution for development, you can manually install the dependencies with `bun install` in the `assets/site` folder.
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
<div class="w-full max-w-5xl">
|
<div class="w-full max-w-5xl">
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
id="canvas"
|
||||||
class="block mx-auto bg-black w-full max-w-[90vw] h-auto mt-5 rounded-xl shadow-[inset_0_0_0_2px_rgba(255,255,255,0.12),0_10px_30px_rgba(0,0,0,0.8)]"
|
oncontextmenu="event.preventDefault()"
|
||||||
|
class="block bg-black w-full max-w-[90vw] h-auto rounded-xl shadow-[inset_0_0_0_2px_rgba(255,255,255,0.12),0_10px_30px_rgba(0,0,0,0.8)]"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
414
web.build.ts
414
web.build.ts
@@ -3,6 +3,22 @@ import { existsSync, promises as fs } from "fs";
|
|||||||
import { platform } from "os";
|
import { platform } from "os";
|
||||||
import { dirname, join, relative, resolve } from "path";
|
import { dirname, join, relative, resolve } from "path";
|
||||||
import { match, P } from "ts-pattern";
|
import { match, P } from "ts-pattern";
|
||||||
|
import { configure, getConsoleSink } from "@logtape/logtape";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const TAILWIND_UPDATE_WINDOW_DAYS = 60; // 2 months
|
||||||
|
|
||||||
|
await configure({
|
||||||
|
sinks: { console: getConsoleSink() },
|
||||||
|
loggers: [
|
||||||
|
{ category: "web.build", lowestLevel: "debug", sinks: ["console"] },
|
||||||
|
{
|
||||||
|
category: ["logtape", "meta"],
|
||||||
|
lowestLevel: "warning",
|
||||||
|
sinks: ["console"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
type Os =
|
type Os =
|
||||||
| { type: "linux"; wsl: boolean }
|
| { type: "linux"; wsl: boolean }
|
||||||
@@ -32,7 +48,7 @@ function log(msg: string) {
|
|||||||
* @param release - Whether to build in release mode.
|
* @param release - Whether to build in release mode.
|
||||||
* @param env - The environment variables to inject into build commands.
|
* @param env - The environment variables to inject into build commands.
|
||||||
*/
|
*/
|
||||||
async function build(release: boolean, env: Record<string, string>) {
|
async function build(release: boolean, env: Record<string, string> | null) {
|
||||||
log(
|
log(
|
||||||
`Building for 'wasm32-unknown-emscripten' for ${
|
`Building for 'wasm32-unknown-emscripten' for ${
|
||||||
release ? "release" : "debug"
|
release ? "release" : "debug"
|
||||||
@@ -40,11 +56,23 @@ async function build(release: boolean, env: Record<string, string>) {
|
|||||||
);
|
);
|
||||||
await $`cargo build --target=wasm32-unknown-emscripten ${
|
await $`cargo build --target=wasm32-unknown-emscripten ${
|
||||||
release ? "--release" : ""
|
release ? "--release" : ""
|
||||||
}`.env(env);
|
}`.env(env ?? undefined);
|
||||||
|
|
||||||
log("Invoking @tailwindcss/cli");
|
// Download the Tailwind CSS CLI for rendering the CSS
|
||||||
// unfortunately, bunx doesn't seem to work with @tailwindcss/cli, so we have to use npx directly
|
const tailwindExecutable = match(
|
||||||
await $`npx --yes @tailwindcss/cli --minify --input styles.css --output build.css --cwd assets/site`;
|
await downloadTailwind(process.cwd(), {
|
||||||
|
version: "latest",
|
||||||
|
force: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.with({ path: P.select() }, (path) => path)
|
||||||
|
.with({ err: P.select() }, (err) => {
|
||||||
|
throw new Error(err);
|
||||||
|
})
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
|
log(`Invoking ${tailwindExecutable}...`);
|
||||||
|
await $`${tailwindExecutable} --minify --input styles.css --output build.css --cwd assets/site`;
|
||||||
|
|
||||||
const buildType = release ? "release" : "debug";
|
const buildType = release ? "release" : "debug";
|
||||||
const siteFolder = resolve("assets/site");
|
const siteFolder = resolve("assets/site");
|
||||||
@@ -110,6 +138,238 @@ async function build(release: boolean, env: Record<string, string>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the Tailwind CSS CLI to the specified directory.
|
||||||
|
* @param dir - The directory to download the Tailwind CSS CLI to.
|
||||||
|
* @returns The path to the downloaded Tailwind CSS CLI, or an error message if the download fails.
|
||||||
|
*/
|
||||||
|
async function downloadTailwind(
|
||||||
|
dir: string,
|
||||||
|
options?: Partial<{
|
||||||
|
version: string; // The version of Tailwind CSS to download. If not specified, the latest version will be downloaded.
|
||||||
|
force: boolean; // Whether to force the download even if the file already exists.
|
||||||
|
}>
|
||||||
|
): Promise<{ path: string } | { err: string }> {
|
||||||
|
const asset = match(os)
|
||||||
|
.with({ type: "linux" }, () => "tailwindcss-linux-x64")
|
||||||
|
.with({ type: "macos" }, () => "tailwindcss-macos-arm64")
|
||||||
|
.with({ type: "windows" }, () => "tailwindcss-windows-x64.exe")
|
||||||
|
.exhaustive();
|
||||||
|
|
||||||
|
const version = options?.version ?? "latest";
|
||||||
|
const force = options?.force ?? false;
|
||||||
|
|
||||||
|
const url =
|
||||||
|
version === "latest" || version == null
|
||||||
|
? `https://github.com/tailwindlabs/tailwindcss/releases/latest/download/${asset}`
|
||||||
|
: `https://github.com/tailwindlabs/tailwindcss/releases/download/${version}/${asset}`;
|
||||||
|
|
||||||
|
// If the GITHUB_TOKEN environment variable is set, use it for Bearer authentication
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (process.env.GITHUB_TOKEN) {
|
||||||
|
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file already exists
|
||||||
|
const path = join(dir, asset);
|
||||||
|
const exists = await fs.exists(path);
|
||||||
|
|
||||||
|
// Check if we should download based on timestamps
|
||||||
|
let shouldDownload = force || !exists;
|
||||||
|
|
||||||
|
if (exists && !force) {
|
||||||
|
try {
|
||||||
|
const fileStats = await fs.stat(path);
|
||||||
|
const fileModifiedTime = fileStats.mtime;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Check if file is older than the update window
|
||||||
|
const updateWindowAgo = new Date(
|
||||||
|
now.getTime() - TAILWIND_UPDATE_WINDOW_DAYS * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fileModifiedTime < updateWindowAgo) {
|
||||||
|
log(
|
||||||
|
`File is older than ${TAILWIND_UPDATE_WINDOW_DAYS} days, checking for updates...`
|
||||||
|
);
|
||||||
|
shouldDownload = true;
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
`File is recent (${fileModifiedTime.toISOString()}), checking if newer version available...`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error checking file timestamp: ${error}, will download anyway`);
|
||||||
|
shouldDownload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to download, check the server's last-modified header
|
||||||
|
if (shouldDownload) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers,
|
||||||
|
method: "HEAD",
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const lastModified = response.headers.get("last-modified");
|
||||||
|
if (lastModified) {
|
||||||
|
const serverTime = new Date(lastModified);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// If server timestamp is in the future, something is wrong - download anyway
|
||||||
|
if (serverTime > now) {
|
||||||
|
log(
|
||||||
|
`Server timestamp is in the future (${serverTime.toISOString()}), downloading anyway`
|
||||||
|
);
|
||||||
|
shouldDownload = true;
|
||||||
|
} else if (exists) {
|
||||||
|
// Compare with local file timestamp (both in UTC)
|
||||||
|
const fileStats = await fs.stat(path);
|
||||||
|
const fileModifiedTime = new Date(fileStats.mtime.getTime());
|
||||||
|
|
||||||
|
if (serverTime > fileModifiedTime) {
|
||||||
|
log(
|
||||||
|
`Server has newer version (${serverTime.toISOString()} vs local ${fileModifiedTime.toISOString()})`
|
||||||
|
);
|
||||||
|
shouldDownload = true;
|
||||||
|
} else {
|
||||||
|
log(`Local file is up to date (${fileModifiedTime.toISOString()})`);
|
||||||
|
shouldDownload = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`No last-modified header available, downloading to be safe`);
|
||||||
|
shouldDownload = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
`Failed to check server headers: ${response.status} ${response.statusText}`
|
||||||
|
);
|
||||||
|
shouldDownload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists && !shouldDownload) {
|
||||||
|
const displayPath = match(relative(process.cwd(), path))
|
||||||
|
// If the path is not a subpath of cwd, display the absolute path
|
||||||
|
.with(P.string.startsWith(".."), (_relative) => path)
|
||||||
|
// Otherwise, display the relative path
|
||||||
|
.otherwise((relative) => relative);
|
||||||
|
|
||||||
|
log(`Tailwind CSS CLI already exists and is up to date at ${displayPath}`);
|
||||||
|
return { path };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
const displayPath = match(relative(process.cwd(), path))
|
||||||
|
// If the path is not a subpath of cwd, display the absolute path
|
||||||
|
.with(P.string.startsWith(".."), (_relative) => path)
|
||||||
|
// Otherwise, display the relative path
|
||||||
|
.otherwise((relative) => relative);
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
log(`Overwriting Tailwind CSS CLI at ${displayPath}`);
|
||||||
|
} else {
|
||||||
|
log(`Downloading updated Tailwind CSS CLI to ${displayPath}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`Downloading Tailwind CSS CLI to ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log(`Fetching ${url}...`);
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
err: `Failed to download Tailwind CSS: ${response.status} ${response.statusText} for '${url}'`,
|
||||||
|
};
|
||||||
|
} else if (!response.body) {
|
||||||
|
return { err: `No response body received for '${url}'` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Content-Length if available
|
||||||
|
const contentLength = response.headers.get("content-length");
|
||||||
|
if (contentLength) {
|
||||||
|
const expectedSize = parseInt(contentLength, 10);
|
||||||
|
if (isNaN(expectedSize)) {
|
||||||
|
return { err: `Invalid Content-Length header: ${contentLength}` };
|
||||||
|
}
|
||||||
|
log(`Expected file size: ${expectedSize} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Writing to ${path}...`);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
const file = Bun.file(path);
|
||||||
|
const writer = file.writer();
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
let downloadedBytes = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
writer.write(value);
|
||||||
|
downloadedBytes += value.length;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
await writer.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate downloaded file size
|
||||||
|
if (contentLength) {
|
||||||
|
const expectedSize = parseInt(contentLength, 10);
|
||||||
|
const actualSize = downloadedBytes;
|
||||||
|
|
||||||
|
if (actualSize !== expectedSize) {
|
||||||
|
// Clean up the corrupted file
|
||||||
|
try {
|
||||||
|
await fs.unlink(path);
|
||||||
|
} catch (unlinkError) {
|
||||||
|
log(`Warning: Failed to clean up corrupted file: ${unlinkError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
err: `File size mismatch: expected ${expectedSize} bytes, got ${actualSize} bytes. File may be corrupted.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`File size validation passed: ${actualSize} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the file executable on Unix-like systems
|
||||||
|
if (os.type !== "windows") {
|
||||||
|
await $`chmod +x ${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure file is not locked; sometimes the runtime is too fast and the file is executed before the lock is released
|
||||||
|
const timeout = Date.now() + 2500; // 2.5s timeout
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
if ((await fs.stat(path)).size > 0) break;
|
||||||
|
} catch {
|
||||||
|
// File might not be ready yet
|
||||||
|
log(`File ${path} is not ready yet, waiting...`);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
} while (Date.now() < timeout);
|
||||||
|
|
||||||
|
// All done!
|
||||||
|
return { path };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
err: `Download failed: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* 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.
|
* @param emsdkDir - The directory containing the Emscripten SDK.
|
||||||
@@ -128,14 +388,73 @@ async function checkEmsdkType(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Activate the Emscripten SDK environment variables.
|
* Activate the Emscripten SDK environment variables.
|
||||||
* Technically, this doesn't actaully activate the environment variables for the current shell,
|
* Technically, this doesn't actually activate the environment variables for the current shell,
|
||||||
* it just runs the environment sourcing script and returns the environment variables for future command invocations.
|
* it just runs the environment sourcing script and returns the environment variables for future command invocations.
|
||||||
* @param emsdkDir - The directory containing the Emscripten SDK.
|
* @param emsdkDir - The directory containing the Emscripten SDK.
|
||||||
* @returns A record of environment variables.
|
* @returns A record of environment variables.
|
||||||
*/
|
*/
|
||||||
async function activateEmsdk(
|
async function activateEmsdk(
|
||||||
emsdkDir: string
|
emsdkDir: string
|
||||||
): Promise<{ vars: Record<string, string> } | { err: string }> {
|
): Promise<{ vars: Record<string, string> | null } | { err: string }> {
|
||||||
|
// If the EMSDK environment variable is set already & the path specified exists, return nothing
|
||||||
|
if (process.env.EMSDK && (await fs.exists(resolve(process.env.EMSDK)))) {
|
||||||
|
log(
|
||||||
|
"Emscripten SDK already activated in environment, using existing configuration"
|
||||||
|
);
|
||||||
|
return { vars: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the emsdk directory exists
|
||||||
|
if (!(await fs.exists(emsdkDir))) {
|
||||||
|
return {
|
||||||
|
err: `Emscripten SDK directory not found at ${emsdkDir}. Please install or clone 'emsdk' and try again.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the emsdk directory 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,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
err: "Emscripten SDK does not appear to be activated/installed properly.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 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") },
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
err: "Emscripten SDK appears to be activated for Windows, but is currently running on a *nix OS.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 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" },
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
err: "Emscripten SDK appears to be activated for *nix, but is currently running on a Windows OS.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Determine the environment script to use based on the OS
|
// Determine the environment script to use based on the OS
|
||||||
const envScript = match(os)
|
const envScript = match(os)
|
||||||
.with({ type: "windows" }, () => join(emsdkDir, "emsdk_env.bat"))
|
.with({ type: "windows" }, () => join(emsdkDir, "emsdk_env.bat"))
|
||||||
@@ -188,81 +507,14 @@ async function main() {
|
|||||||
const release = process.env.RELEASE !== "0";
|
const release = process.env.RELEASE !== "0";
|
||||||
const emsdkDir = resolve("./emsdk");
|
const emsdkDir = resolve("./emsdk");
|
||||||
|
|
||||||
// Check if Emscripten is already activated in the environment
|
// Activate the Emscripten SDK (returns null if already activated)
|
||||||
const emscriptenAlreadyActivated =
|
const vars = match(await activateEmsdk(emsdkDir))
|
||||||
process.env.EMSCRIPTEN || process.env.EMSDK;
|
.with({ vars: P.select() }, (vars) => vars)
|
||||||
|
.with({ err: P.any }, ({ err }) => {
|
||||||
let vars: Record<string, string>;
|
log("Error activating Emscripten SDK: " + err);
|
||||||
|
|
||||||
if (emscriptenAlreadyActivated) {
|
|
||||||
log(
|
|
||||||
"Emscripten SDK already activated in environment, using existing configuration"
|
|
||||||
);
|
|
||||||
vars = process.env as Record<string, string>;
|
|
||||||
} else {
|
|
||||||
// Ensure the emsdk directory exists before attempting to activate or use it
|
|
||||||
if (!(await fs.exists(emsdkDir))) {
|
|
||||||
log(
|
|
||||||
`Emscripten SDK directory not found at ${emsdkDir}. Please install or clone 'emsdk' and try again.`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
})
|
||||||
|
.exhaustive();
|
||||||
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
|
// Build the application
|
||||||
await build(release, vars);
|
await build(release, vars);
|
||||||
|
|||||||
Reference in New Issue
Block a user