SOURCE / PINNED RELEASE
Made of little things.
Powder Tool V600Billion
- Release
- 142767edcab8…
- Author-recorded commit
- 6d92971effd0…
- License
- LICENSE
- Author’s source reference
- nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy
Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
#!/usr/bin/env bash
# Build The Powder Toy's WebAssembly without threads, into vendor/.
#
# scripts/build-wasm.sh build, then check the result against upstream.json
# scripts/build-wasm.sh --pin build, then write the result's hashes into upstream.json
#
# Upstream's own web release is built with pthreads and only runs in a
# cross-origin isolated page. This builds the same pinned source the same way
# (upstream's release options, Emscripten 3.1.72, LTO) with these changes:
#
# patches/tpt-libs-single-thread.patch the prebuilt libraries (fftw, jsoncpp,
# lua) compiled without USE_PTHREADS
# patches/the-powder-toy-single-thread.patch the game with -Demscripten_threads=false:
# background work on the main thread,
# network tasks as request steps
# patches/the-powder-toy-napplet-bridge.patch two exported functions for the page's
# own controls (Lua, which window is on top)
#
# Needs git, python3, ninja, cmake, make and node on PATH, and Meson at exactly
# the version in upstream.json (build.meson; newer Meson generates other code
# for this build type, so another version gives other bytes). Emscripten is
# always installed into the work directory, never taken from an active emsdk.
# Everything is fetched with git and checked against the commits in
# upstream.json; Emscripten's ports that come from GitHub archives (zlib, SDL2,
# bzip2) are seeded from git the same way.
#
# The result does not depend on where it is built: Emscripten and its cache
# live at fixed places in the work directory (its libraries and ports are
# built there, the same way every time), and every compiler call maps the work
# directory to /wasm-build, so no path of this machine ends up in the wasm
# (fftw and SDL name their source files in assertion messages). Emscripten
# names its own library sources relative to its cache, which is why it may not
# come from elsewhere: an emsdk outside the work directory put
# "../../../../../../emsdk/..." into the wasm of one machine only.
#
# Work directory: $POWDER_WASM_WORK, default .wasm-build/ in this package
# (gitignored), without spaces in its path. Takes 15 to 25 minutes the first time.
set -euo pipefail
IFS=$'\n\t'
here=$(cd "$(dirname "$0")/.." && pwd)
work=${POWDER_WASM_WORK:-$here/.wasm-build}
pin=no
if [[ ${1-} == --pin ]]; then pin=yes; fi
pinned() { node -e 'const u = require(process.argv[1]); let v = u; for (const k of process.argv[2].split(".")) v = v[k]; process.stdout.write(String(v));' "$here/upstream.json" "$1"; }
# Clone a repository at a tag and insist on the pinned commit.
checkout() {
local url=$1 tag=$2 commit=$3 dir=$4
if [[ ! -d $dir/.git ]]; then
git -c advice.detachedHead=false clone --quiet --depth 1 --branch "$tag" "$url" "$dir"
fi
local got
got=$(git -C "$dir" rev-parse HEAD)
if [[ $got != "$commit" ]]; then
>&2 echo "$url $tag is $got, upstream.json pins $commit"
exit 1
fi
}
mkdir -p "$work"
work=$(cd "$work" && pwd)
if [[ $work =~ [[:space:]] ]]; then
>&2 echo "the work directory must not contain spaces: $work"
exit 1
fi
# clang reads this for every compile, whoever calls it (emcc, CMake, make, Meson).
export CCC_OVERRIDE_OPTIONS="# +-ffile-prefix-map=$work=/wasm-build"
# Meson, exactly the pinned version
meson_version=$(pinned build.meson)
if [[ $(meson --version 2>/dev/null) != "$meson_version" ]]; then
>&2 echo "Meson $meson_version is needed (found: $(meson --version 2>/dev/null || echo none)); for example: pipx install meson==$meson_version"
exit 1
fi
# Emscripten, in the work directory only, with its cache inside it
emsdk_version=$(pinned build.emsdk)
if [[ ! -d $work/emsdk ]]; then
git -c advice.detachedHead=false clone --quiet --depth 1 --branch "$emsdk_version" https://github.com/emscripten-core/emsdk.git "$work/emsdk"
fi
(cd "$work/emsdk" && ./emsdk install "$emsdk_version" && ./emsdk activate "$emsdk_version") >/dev/null
# shellcheck disable=SC1091
source "$work/emsdk/emsdk_env.sh" >/dev/null 2>&1
emroot=$work/emsdk/upstream/emscripten
export EM_CACHE=$emroot/cache
if [[ $(command -v emcc) != "$emroot/emcc" ]]; then
>&2 echo "emcc resolves to $(command -v emcc), not the work directory's $emroot/emcc"
exit 1
fi
# Emscripten reads its list of ports in directory order (os.listdir), and links
# the port libraries (bzip2, libpng, zlib, SDL2) in that order: the functions
# land elsewhere in the wasm on another file system. Sorted, the order is the
# same on every machine.
ports_py=$emroot/tools/ports/__init__.py
sed -i -E 's/for filename in os\.listdir\((ports_dir|contrib_dir)\):/for filename in sorted(os.listdir(\1)):/' "$ports_py"
if [[ $(grep -c 'for filename in sorted(os.listdir(' "$ports_py") != 2 ]] || grep -q 'for filename in os.listdir(' "$ports_py"; then
>&2 echo "could not sort Emscripten's port list in $ports_py"
exit 1
fi
echo "emcc: $(emcc --version | head -1)"
for tool in meson ninja cmake make python3; do echo "$tool: $("$tool" --version | head -1)"; done
# Emscripten ports that upstream uses and Emscripten fetches as GitHub archives:
# put the same trees in its cache from git, marked as already unpacked.
ports=$("$emroot/em-config" CACHE)/ports
seed_port() {
local name=$1 subdir=$2 url=$3 tag=$4 commit=$5 archive=$6
if [[ -f $ports/$name/.emscripten_url ]] && [[ $(cat "$ports/$name/.emscripten_url") == "$archive" ]]; then
return
fi
checkout "$url" "$tag" "$commit" "$work/ports/$name"
rm -rf "${ports:?}/${name:?}"
mkdir -p "$ports/$name"
git -C "$work/ports/$name" archive --format=tar --prefix="$subdir/" HEAD | tar -x -C "$ports/$name"
printf '%s\n' "$archive" > "$ports/$name/.emscripten_url"
}
for port in zlib sdl2 bzip2; do
seed_port "$port" "$(pinned "build.ports.$port.subdir")" "$(pinned "build.ports.$port.repo")" \
"$(pinned "build.ports.$port.tag")" "$(pinned "build.ports.$port.commit")" "$(pinned "build.ports.$port.archive")"
done
# The prebuilt libraries, without threads
libs_vtag=v00000000000000
libs_dir=$work/tpt-libs/temp/tpt-libs-prebuilt-wasm32-emscripten-emscripten-static-release-$libs_vtag
if [[ ! -f $libs_dir/meson.build ]]; then
rm -rf "$work/tpt-libs"
checkout "$(pinned build.libraries.repo)" "$(pinned build.libraries.tag)" "$(pinned build.libraries.commit)" "$work/tpt-libs"
git -C "$work/tpt-libs" apply "$here/patches/tpt-libs-single-thread.patch"
# Its last step packs a zip with 7z; the directory is all that is needed.
mkdir -p "$work/bin"
printf '#!/bin/sh\nexit 0\n' > "$work/bin/7z"
chmod +x "$work/bin/7z"
(
cd "$work/tpt-libs"
export PATH=$work/bin:$PATH EMROOT=$emroot BSH_NO_PACKAGES=1 BSH_BUILD_PLATFORM=linux \
BSH_HOST_ARCH=wasm32 BSH_HOST_PLATFORM=emscripten BSH_HOST_LIBC=emscripten \
BSH_STATIC_DYNAMIC=static BSH_DEBUG_RELEASE=release BSH_VTAG=$libs_vtag
./.github/build.sh
) > "$work/tpt-libs.log" 2>&1 || { >&2 echo "library build failed, see $work/tpt-libs.log"; exit 1; }
fi
# The game, with the patches in upstream.json's order; a checkout patched
# with another set starts over.
src=$work/the-powder-toy
game_patches=()
while IFS= read -r patch; do game_patches+=("$here/$patch"); done < <(
node -e 'for (const p of require(process.argv[1]).build.patches) if (p.startsWith("patches/the-powder-toy-")) console.log(p);' "$here/upstream.json")
patched_with=$(cat "${game_patches[@]}" | sha256sum | cut -d' ' -f1)
if [[ ! -f $src/.napplet-patched ]] || [[ $(cat "$src/.napplet-patched") != "$patched_with" ]]; then
rm -rf "$src"
checkout "$(pinned source)" "$(pinned tag)" "$(pinned commit)" "$src"
for patch in "${game_patches[@]}"; do git -C "$src" apply "$patch"; done
printf '%s\n' "$patched_with" > "$src/.napplet-patched"
fi
rm -rf "$src/subprojects/$(basename "$libs_dir")"
cp -r "$libs_dir" "$src/subprojects/"
# Upstream's release configuration (.github/build.sh for wasm32-emscripten-emscripten-static),
# plus the two switches this build is about.
rm -rf "$src/build"
(
cd "$src"
meson setup \
-Dbuildtype=debugoptimized \
-Dstrip=false \
-Db_staticpic=false \
-Dmod_id=0 \
-Dpackage_mode=emscripten \
-Dstatic=prebuilt \
-Dignore_updates=false \
-Dlto=true \
--cross-file=.github/emscripten-ghactions.ini \
"-Dc_args=['-ffunction-sections','-fdata-sections']" \
"-Dcpp_args=['-ffunction-sections','-fdata-sections']" \
"-Dc_link_args=['-Wl,--gc-sections','-gseparate-dwarf=powder.wasm.dbg']" \
"-Dcpp_link_args=['-Wl,--gc-sections','-gseparate-dwarf=powder.wasm.dbg']" \
-Demscripten_threads=false \
-Dtpt_libs_vtag=$libs_vtag \
build
cd build
meson compile
) > "$work/the-powder-toy.log" 2>&1 || { >&2 echo "game build failed, see $work/the-powder-toy.log"; exit 1; }
mkdir -p "$here/vendor"
cp "$src/build/powder.js" "$src/build/powder.wasm" "$here/vendor/"
node - "$here" "$pin" <<'NODE'
const { createHash } = require('node:crypto');
const { readFileSync, writeFileSync } = require('node:fs');
const [here, pin] = process.argv.slice(2);
const file = `${here}/upstream.json`;
const upstream = JSON.parse(readFileSync(file, 'utf8'));
let same = true;
for (const name of ['powder.js', 'powder.wasm']) {
const got = createHash('sha256').update(readFileSync(`${here}/vendor/${name}`)).digest('hex');
const want = upstream.build.files[name];
console.log(`${name}: ${got}${got === want ? ' (as pinned)' : ` (upstream.json pins ${want})`}`);
same &&= got === want;
upstream.build.files[name] = got;
}
if (pin === 'yes') {
writeFileSync(file, `${JSON.stringify(upstream, null, 2)}\n`);
console.log('pinned in upstream.json');
} else if (!same) {
console.error('The build differs from the pinned one. Check the toolchain; scripts/build-wasm.sh --pin takes it on purpose.');
process.exit(1);
}
NODE
