#!/usr/bin/env bash # Compila o FreeCol (BrowserCol) do fredygames.com: o FreeCol (The FreeCol Team, GPL-2.0-or-later) com o motor # (modelo, servidor e IA em Java) compilado para WebAssembly GC pelo TeaVM e a interface em TypeScript/React/PixiJS do # BrowserCol (shivan2418/browsercol, GPL-2.0-or-later), sem Docker. Este script, os patches e o gerador de arte fazem # parte do codigo-fonte publicado junto do jogo. # # src/FredGames.Bundler/Builds/build-freecol.sh # # Precisa de artifacts/tools/: JDK 21 (Temurin, jdk-jdk-21.0.13+9), Gradle 9.8.2 (gradle-9.8.2, o do Dockerfile do # BrowserCol), Node (node-v24.21.0; o Dockerfile usa 24.18.0, com o pnpm 12.16.1 que o Corepack baixa), o Python com # Pillow (venv) e o oggdec/wav2ogg do pygame-music (Builds/pygame-music/README). Baixa o Gradle plugin do TeaVM 0.16.0, as # bibliotecas e os pacotes do pnpm-lock.yaml (travado). Caches em artifacts/build/cache/*-freecol (apague depois). # # Fontes (SHA-256 conferido, guardados em artifacts/cache/freecol/): o tarball do commit 4428b2c do BrowserCol (2 MB) e o # do commit 1c272e8 do FreeCol (940 MB: o repositorio todo, com 850 MB de srcdata e o site do projeto). Do FreeCol entram # so src/, jars/, data/ e os textos de licenca; o build gera tambem freecol-src-subset.tar.gz, o fonte que este site # hospeda: src/, jars/, data/base, data/rules, data/strings e as licencas (os recursos de data/default, imagens e sons, # o site serve como estao, e a musica so recomprimida). # # O que muda do BrowserCol/FreeCol (browsercol-fredygames.patch e freecol-fredygames.patch, mais os passos abaixo): # - a imagem do primeiro desembarque (landing.png: David Rumsey Map Collection, CC BY-NC-SA) e o alert.ogg (Freesound, # CC BY-NC) saem; no lugar entram arte e som nossos (make-art.py, GPL-2.0-or-later). As outras cinco imagens do # Rumsey (tipos de pai fundador), o bg_map1.jpg, o ShadowedBlack.ttf (sem licenca) e o video de abertura (sem origem) # o BrowserCol nao usa e ficam fora do pacote (e do fonte hospedado); # - a musica (CC BY 4.0) e recomprimida em Ogg Vorbis de ~100 kbps (de ~500 kbps) para caber (os creditos dizem); # - so as traducoes en (base), pt_BR e pt_PT vao no site (as outras 58 ficam de fora); # - sem o tileset de teste (so os dois da tela de opcoes); # - credits.tsx: creditos dos itens acima, aviso de versao modificada e feita com ajuda de IA; rules-help.tsx e a # descricao das regras Classic sem o nome do jogo de 1994; # - teavm.ts: os dois modulos wasm (10 e 14 MB) vao em .gz e a pagina os descomprime (o CloudFront nao comprime # arquivos acima de 10 MB); # - o index.html ganha uma Content-Security-Policy que bloqueia qualquer conexao fora da origem. # # Saida: artifacts/build/freecol-4428b2c/web.tar.gz (a pasta site/) e freecol-src-subset.tar.gz; tar e gzip # deterministicos. set -euo pipefail BC=4428b2ce82d1f3517885495984a297890ed2bab6 BC_SHA256=7df1bc96c464e9fefe361d309a794143ccf962409b040e520b1bf95c25dc5796 FC=1c272e80d2f8946e096063f6d88eb945a44f789d FC_SHA256=6315bd5b8fa13ce1d4e274c92081e8860e7a4336c6824ca3457bec8f1144edef MUSIC_QUALITY=0.25 LANGS="pt_BR pt_PT" SLUG=freecol here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" root="$(git -C "$here" rev-parse --show-toplevel 2>/dev/null || pwd)" tools="$root/artifacts/tools" out="$root/artifacts/build/$SLUG-${BC:0:7}" cache="$root/artifacts/cache/$SLUG" aux="$here/$SLUG" [ -d "$aux" ] || aux="$here" # na pasta de fontes do site, os arquivos ficam ao lado do script export JAVA_HOME="$tools/jdk-jdk-21.0.13+9/Contents/Home" export GRADLE_USER_HOME="$root/artifacts/build/cache/gradle-$SLUG" export COREPACK_HOME="$root/artifacts/build/cache/corepack-$SLUG" export npm_config_cache="$root/artifacts/build/cache/npm-$SLUG" export npm_config_store_dir="$root/artifacts/build/cache/pnpm-store-$SLUG" export BUILD_COMMIT="$BC" export PATH="$out/bin:$JAVA_HOME/bin:$tools/gradle-9.8.2/bin:$tools/node-v24.21.0/bin:$PATH" PY="$tools/venv/bin/python" OGGDEC="$tools/pygame-music/oggdec" WAV2OGG="$tools/pygame-music/wav2ogg" for f in "$JAVA_HOME/bin/java" "$tools/gradle-9.8.2/bin/gradle" "$tools/node-v24.21.0/bin/node" "$PY" "$OGGDEC" "$WAV2OGG"; do [ -x "$f" ] || { echo "Falta $f (ver o cabecalho)." >&2; exit 1; } done fetch() { # fetch -> caminho local name="$1" url="$2" sha="$3" file if [ -f "$here/$name" ]; then file="$here/$name"; else file="$cache/$name" if [ ! -f "$file" ]; then mkdir -p "$cache"; curl -fsSL -o "$file.part" "$url"; mv "$file.part" "$file"; fi fi echo "$sha $file" | shasum -a 256 -c - >/dev/null || { echo "SHA-256 de $name diverge." >&2; exit 1; } echo "$file" } bc_tar="$(fetch browsercol-$BC.tar.gz "https://codeload.github.com/shivan2418/browsercol/tar.gz/$BC" "$BC_SHA256")" fc_tar="$(fetch freecol-$FC.tar.gz "https://codeload.github.com/FreeCol/freecol/tar.gz/$FC" "$FC_SHA256")" rm -rf "$out" mkdir -p "$out/src" "$out/bin" "$GRADLE_USER_HOME" "$COREPACK_HOME" cd "$out/src" tar xzf "$bc_tar" --strip-components=1 patch -p1 -s < "$aux/browsercol-fredygames.patch" # FreeCol: so o que o build usa (o tarball tem 1,2 GB descompactado). fcdir="freecol-$FC" mkdir freecol tar xzf "$fc_tar" -C freecol --strip-components=1 \ "$fcdir/src" "$fcdir/jars" "$fcdir/data" "$fcdir/LICENSE" "$fcdir/README.md" "$fcdir/CHANGELOG.md" "$fcdir/schema" mkdir freecol/.git && echo "$FC" > freecol/.git/HEAD # o build.gradle do motor confere que o clone esta no commit pinado # Fonte hospedado (antes de tirar ou trocar qualquer coisa): o FreeCol exato, sem os recursos que o site serve como estao. "$PY" -I - "$out/src/freecol" "$out/freecol-src-subset.tar.gz" <<'PY' import gzip, io, os, sys, tarfile src, dest = sys.argv[1:3] keep = ("src/", "jars/", "data/base/", "data/rules/", "data/strings/", "schema/") names = [] for d, _, fs in os.walk(src): for f in fs: rel = os.path.relpath(os.path.join(d, f), src) if rel.startswith(".git/"): continue if rel in ("LICENSE", "README.md", "CHANGELOG.md") or rel.startswith(keep): if rel in ("data/base/resources/fonts/ShadowedBlack.ttf",): # sem licenca: nao vai continue names.append(rel) names.sort() raw = io.BytesIO() with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as tar: for n in names: data = open(os.path.join(src, n), "rb").read() ti = tarfile.TarInfo("freecol/" + n) ti.size = len(data); ti.mtime = 0; ti.mode = 0o644; ti.uid = ti.gid = 0; ti.uname = ti.gname = "" tar.addfile(ti, io.BytesIO(data)) open(dest, "wb").write(gzip.compress(raw.getvalue(), 9, mtime=0)) PY # Tira o que nao vai: fonte sem licenca, imagens do Rumsey, video, alerta; traducoes alem de pt_BR e pt_PT. cd freecol rm -f data/base/resources/fonts/ShadowedBlack.ttf data/default/resources/video/freecol-opening.video.ogg \ data/default/resources/sound/event/alert.ogg for f in trade exploration military political religious; do rm -f "data/default/resources/images/ui/$f.png"; done find data -name bg_map1.jpg -delete for f in data/strings/FreeColMessages_*.properties; do id="${f##*FreeColMessages_}"; id="${id%.properties}" case " $LANGS " in *" $id "*) ;; *) rm -f "$f" ;; esac done patch -p1 -s < "$aux/freecol-fredygames.patch" # Arte e som nossos no lugar dos do Rumsey e do Freesound. art="$out/art" "$PY" -I "$aux/make-art.py" "$art" cp "$art/landing.png" data/default/resources/images/ui/landing.png "$WAV2OGG" "$art/alert.wav" data/default/resources/sound/event/alert.ogg 0.3 # Musica (CC BY 4.0) recomprimida. tmp="$out/music-tmp"; mkdir -p "$tmp" for f in data/default/resources/music/default/*.ogg; do "$OGGDEC" "$f" "$tmp/a.wav" "$WAV2OGG" "$tmp/a.wav" "$f" "$MUSIC_QUALITY" done rm -rf "$tmp" cd .. # Motor: os dois modulos wasm (servidor e cliente). gradle -p engine --console=plain -q modules -Prelease # Interface (os passos do build.sh do BrowserCol, sem o tileset de teste). corepack enable pnpm --install-directory "$out/bin" >/dev/null gen=ui/generated rm -rf "$gen" tools/copy-modules.sh node ui/scripts/build-data.ts freecol/data "$gen/data.json" "$gen/public/strings" node ui/scripts/build-art.ts freecol/data "$gen/public/art" node ui/scripts/build-art.ts freecol/data "$gen/public/art" --manifest freecol-plus.json ui/tilesets/freecol-plus node ui/scripts/build-audio.ts freecol/data "$gen/public/audio" mkdir -p "$gen/public/brand" "$gen/public/presets" cp ui/brand/globe-*.svg "$gen/public/brand/" cp ui/presets/*.json "$gen/public/presets/" ( cd ui pnpm install --frozen-lockfile node scripts/build-icons.ts brand generated/public/icons pnpm exec vp build --mode release ) # Pos-processamento: wasm em .gz, politica de seguranca no index.html, pacote deterministico. python3 -I - "$out/src/dist" "$out/web.tar.gz" <<'PY' import gzip, io, os, re, sys, tarfile src, dest = sys.argv[1:3] names = [] for d, _, fs in os.walk(src): for f in fs: names.append(os.path.relpath(os.path.join(d, f), src)) names.sort() csp = ("default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; " "img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self' data:; worker-src 'self' blob:; " "connect-src 'self' data: blob:") raw = io.BytesIO() with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as tar: for n in names: data = open(os.path.join(src, n), "rb").read() if n.endswith(".wasm"): n += ".gz" data = gzip.compress(data, 9, mtime=0) elif n == "index.html": html = data.decode("utf-8") assert "" in html html = html.replace("", '\n ' % csp, 1) data = html.encode("utf-8") ti = tarfile.TarInfo("site/" + n) ti.size = len(data); ti.mtime = 0; ti.mode = 0o644; ti.uid = ti.gid = 0; ti.uname = ti.gname = "" tar.addfile(ti, io.BytesIO(data)) with open(dest, "wb") as f: f.write(gzip.compress(raw.getvalue(), 9, mtime=0)) PY shasum -a 256 "$out/web.tar.gz" "$out/freecol-src-subset.tar.gz" echo "Pronto: $out/web.tar.gz"