Remove tup: delete Lua build config, tupctime, and tup state

The build is now driven by tools/build (see Buildfile.ts). tupctime is
obsolete: the new tool hashes content behind a stat cache, so ctime/mtime
churn without content changes no longer causes rebuilds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Christopher Monsanto
2026-08-15 22:09:51 -04:00
parent c6c336dc2d
commit 02194b17f0
14 changed files with 28 additions and 702 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,6 @@
/build/
.tup
node_modules
pnpm-debug.log
tup.config
.cache/
.build/
build.config

View File

@@ -6,14 +6,13 @@
This project depends on
- [tup](http://gittup.org/tup/)
- [ImageMagick](http://www.imagemagick.org/) >= 7
- [gifsicle](https://www.lcdf.org/gifsicle/)
- [AdvPng](http://www.advancemame.it/doc-advpng.html) (optional)
- [OptiPNG](http://optipng.sourceforge.net/) (optional)
- [pngquant](https://pngquant.org/) (optional)
- [pnpm](https://pnpm.js.org)
- [node.js](https://nodejs.org) >= 24
- [wine](https://www.winehq.org/) (optional)
- cwebp
### Windows
@@ -23,21 +22,10 @@ Windows binaries of these dependencies can be found on the download pages of the
### Linux
```
$ sudo apt install nodejs imagemagick advancecomp optipng pngquant wine webp
$ sudo apt install nodejs imagemagick gifsicle advancecomp optipng pngquant webp
$ sudo npm install -g pnpm
```
Build tup from source:
```
$ sudo apt install build-essential pkg-config fuse3 libfuse3-dev libpcre3-dev
$ git clone git://github.com/gittup/tup.git
$ cd tup
$ ./bootstrap.sh
$ sudo cp tup /usr/local/bin/tup
$ sudo cp tup.1 /usr/local/share/man
```
You may have to build imagemagick from source to get version 7.
```
@@ -55,8 +43,7 @@ $ sudo ldconfig /usr/local/lib
Using [`brew`](https://brew.sh/) on a macOS:
```
$ brew cask install osxfuse wine-stable
$ brew install tup imagemagick advancecomp optipng pngquant
$ brew install imagemagick gifsicle advancecomp optipng pngquant webp
```
## Building
@@ -64,34 +51,44 @@ $ brew install tup imagemagick advancecomp optipng pngquant
Install dependencies once with `pnpm install`. Then, to build:
```
$ tup
$ node tools/build/index.ts
```
The rules live in `Buildfile.ts`. Build state (content hashes, rule records)
is kept in `.build/`; outputs of removed rules are deleted automatically, and
renamed sources are detected and their outputs copied instead of rebuilt.
Useful flags: `-j <n>` parallelism, `-n` dry run, `-v` verbose,
`--adopt` record already-existing `build/` outputs as up to date instead of
rebuilding them (useful when `build/` was produced elsewhere).
## Configuration
Build settings are configurable in `tup.config`.
Build settings are configurable in `build.config` (not tracked by git).
- `CONFIG_DEFAULT_OPTIPNG`: Command line to pass to `optipng`.
- `CONFIG_DEFAULT_ADVPNG`: Command line to pass to `advpng`.
- `CONFIG_DEFAULT_PNGQUANT`: Command line to pass to `pngquant`.
- `DEFAULT_OPTIPNG`: Command line to pass to `optipng`.
- `DEFAULT_ADVPNG`: Command line to pass to `advpng`.
- `DEFAULT_PNGQUANT`: Command line to pass to `pngquant`.
There are src-specific versions of these settings:
- `CONFIG_TRAINERS_<PROGRAM>`: Compression options for `trainers/` only.
- `CONFIG_DEX_<PROGRAM>`: Compression options for `dex/` only.
- `CONFIG_MODELS_<PROGRAM>`: Compression options for `models/` only.
- `CONFIG_SPRITESHEET_<PROGRAM>`: Compression options for spritesheets only.
- `CONFIG_MINISPRITE_<PROGRAM>`: Compression options for `minisprites/` only.
- `TRAINERS_<PROGRAM>`: Compression options for `trainers/` only.
- `DEX_<PROGRAM>`: Compression options for `dex/` only.
- `MODELS_<PROGRAM>`: Compression options for `models/` only.
- `SPRITESHEET_<PROGRAM>`: Compression options for spritesheets only.
- `MINISPRITE_<PROGRAM>`: Compression options for `minisprites/` only.
For example, these settings reflect the compression settings for the files chaos uploaded in `src/`:
```
CONFIG_DEFAULT_OPTIPNG=-o7
CONFIG_DEFAULT_ADVPNG=-z4 -i5000
DEFAULT_OPTIPNG=-o7
DEFAULT_ADVPNG=-z4 -i5000
```
## Gotchas
- Tup, like Git, tracks files, not directories. If you `readdir()` and forget to declare a dependency it won't catch it, like it would for `read()`. You can work around this by having build tools `stat()` any filenames they acquire.
- The build tool only tracks the inputs a rule declares. If a build tool reads
files that aren't on its command line (e.g. it does a `readdir()`), declare
them with the rule's `deps:` in `Buildfile.ts` so changes are detected.
## License

View File

View File

@@ -1,257 +0,0 @@
-- Generate uniform size minisprites
foreach_rule(
"src/minisprites/pokemon/gen6/*.png",
{
display="pad g6 minisprite %f",
pad{w=40, h=30},
compresspng{config="MINISPRITE"}
},
"build/gen6-minisprites-padded/%b"
)
foreach_rule(
"src/minisprites/items/*.png",
{
display="pad item minisprite %f",
pad{w=24, h=24},
compresspng{config="MINISPRITE"}
},
"build/item-minisprites-padded/%b"
)
foreach_rule(
"src/minisprites/pokemon/gen6/*.png",
{
display="trim g6 minisprite %f",
trimimg{},
compresspng{config="MINISPRITE"}
},
"build/gen6-minisprites-trimmed/%b"
)
foreach_rule(
"src/minisprites/items/*.png",
{
display="trim item minisprite %f",
trimimg{},
compresspng{config="MINISPRITE"}
},
"build/item-minisprites-trimmed/%b"
)
-- Gen 9
foreach_rule(
"src/gen9species/*.png",
{
display="96x96 %f",
-- TODO, add customizable compression for gif
-- ... or investigate using webp instead of both png/gif here
"magick convert %f -trim +repage -resize 90x90 %o",
"gifsicle -O3 -b %o"
},
"build/gen9-modelslike/%B.gif"
)
-- Gen 10
foreach_rule(
"src/champions/*.png",
{
display="96x96 %f",
-- TODO, add customizable compression for gif
-- ... or investigate using webp instead of both png/gif here
"magick convert %f -trim +repage -resize 90x90 %o",
"gifsicle -O3 -b %o"
},
"build/gen10-modelslike/%B.gif"
)
-- Gen 5 CAPs...
foreach_rule(
"src/sprites/gen5/*.png",
{
-- TODO, add customizable compression for gif
-- ... or investigate using webp instead of both png/gif here
"magick convert %f %o",
"gifsicle -O3 -b %o"
},
"build/gen5-gif/%B.gif"
)
-- PS spritesheet
rule(
"ps-pokemon.sheet.mjs",
{
display="ps pokemon sheet",
"node tools/sheet/index.ts %f %o",
compresspng{config="SPRITESHEET"}
},
"build/ps/pokemonicons-sheet.png"
)
-- TODO: reenable when trainers are moved
-- rule{
-- display="ps trainers sheet",
-- {"ps-trainers.sheet.mjs"},
-- {
-- "node tools/sheet/index.ts %f %o",
-- compresspng{config="SPRITESHEET"}
-- },
-- {"build/ps/trainers-sheet.png"}
-- }
rule(
"ps-items.sheet.mjs",
{
display="ps items sheet",
"node tools/sheet/index.ts %f %o",
compresspng{config="SPRITESHEET"}
},
"build/ps/itemicons-sheet.png"
)
-- PS pokeball icons
local balls = {
"src/_uncategorized/noncanonical/ui/battle/Ball-Normal.png",
"src/_uncategorized/noncanonical/ui/battle/Ball-Sick.png",
"src/_uncategorized/noncanonical/ui/battle/Ball-Null.png",
}
rule(
balls,
{
display="pokemonicons-pokeball-sheet",
"magick convert -background transparent -gravity center -extent 40x30 %f +append %o",
compresspng{config="SPRITESHEET"}
},
"build/ps/pokemonicons-pokeball-sheet.png"
)
-- Smogdex minisprites (webp)
foreach_rule(
spriteglob({"src/minisprites/pokemon/gen6/*", "src/minisprites/items/*"}, {a = false}),
{
display="webp minisprite %f",
"cwebp -z 9 %f -o %o"
},
"build/smogon/minisprites/%B.webp"
)
-- Smogdex spritesheet
rule(
spriteglob({"src/minisprites/pokemon/gen6/*", "src/minisprites/items/*"}, {a = false}),
{
display="smogdex sheet",
"node tools/smogdexspritesheet/index.ts --image build/smogon/spritesheet.png --stylesheet build/smogon/spritesheet.css -- %f",
"cwebp -z 9 build/smogon/spritesheet.png -o build/smogon/spritesheet.webp",
"rm build/smogon/spritesheet.png"
},
{"build/smogon/spritesheet.webp", "build/smogon/spritesheet.css"}
)
-- Smogdex social images
local input = spriteglob({"src/models/*"}, {b = false, s = false})
-- Copied from the CAP bit, dedup, should probably make a routine for it
local dexSet = {}
for file in iter(input) do
dexSet[tup.base(file)] = true
end
for file in iter(spriteglob({"src/gen9species/*"}, {b = false, s = false})) do
local base = tup.base(file)
if dexSet[base] then
goto continue
end
input += file
dexSet[base] = true
::continue::
end
foreach_rule(
input,
{
display="fbsprite %f",
"magick convert \"%f[0]\" -trim -resize 150x150 -background white -gravity center -extent 198x198 -bordercolor black -border 1 %o",
compresspng{config="MODELS"}
},
"build/smogon/fbsprites/xy/%B.png"
)
foreach_rule(
input,
{
display="twittersprite %f",
"magick convert \"%f[0]\" -trim -resize 115x115 -background white -gravity center -extent 120x120 %o",
compresspng{config="MODELS"}
},
"build/smogon/twittersprites/xy/%B.png"
)
-- Trainers
-- TODO: reenable when trainers are moved
-- foreach_rule{
-- display="pad trainer %f",
-- {"src/canonical/trainers/*"},
-- {
-- pad{w=80, h=80},
-- compresspng{config="TRAINERS"}
-- },
-- {"build/padded-trainers/canonical/%b"}
-- }
-- Padded Dex
local dexOutput = foreach_rule(
"src/dex/*",
{
display="pad dex %f",
pad{w=120, h=120},
compresspng{config="DEX"}
},
"build/padded-dex/%b"
)
-- Build missing CAP dex
local dexSet = {}
for file in iter(dexOutput) do
dexSet[tup.base(file)] = true
end
local dexMissing = {}
for file in iter(spriteglob(
{"src/sprites/gen5/*.gif", "src/models/*.gif"},
{b = false, s = false})) do
local base = tup.base(file)
if dexSet[base] then
goto continue
end
dexMissing += file
dexSet[base] = true
::continue::
end
foreach_rule(
dexMissing,
{
display="missing dex %B",
"magick convert \"%f[0]\" -trim %o",
"magick mogrify -background transparent -gravity center -resize \"120x120>\" -extent 120x120 %o",
compresspng{config="DEX"}
},
"build/padded-dex/%B.png"
)

View File

@@ -1,53 +0,0 @@
--tup.include("util/strict.lua")
tup.include("util/lua-ext.lua")
tup.include("util/tup-ext.lua")
tup.include("util/sprites.lua")
ROOTDIR = tup.getcwd()
function pad(opts)
return rep{
"magick convert ${input} -background transparent -gravity center -extent ${w}x${h} ${output}",
input = opts.input or "%f",
output = opts.output or "%o",
w = opts.w,
h = opts.h
}
end
function trimimg(opts) -- Can't just be trim because of the string function...
return rep{
"magick convert ${input} -trim ${output}",
input = opts.input or "%f",
output = opts.output or "%o",
}
end
local function compressopts(program, copts)
copts.pngquant = getconfig(program .. "_PNGQUANT") or copts.pngquant
copts.optipng = getconfig(program .. "_OPTIPNG") or copts.optipng
copts.advpng = getconfig(program .. "_ADVPNG") or copts.advpng
end
function compresspng(opts)
local cmds = {}
local output = opts.output or "%o"
local copts = {}
compressopts("DEFAULT", copts)
if opts.config then
compressopts(opts.config, copts)
end
if copts.pngquant then
-- -f -o necessary to overwrite existing file
cmds += rep{"pngquant -f -o ${output} ${opts} ${output}", opts=copts.pngquant, output=output}
end
if copts.optipng then
cmds += rep{"optipng -q ${opts} ${output}", opts=copts.optipng, output=output}
end
if copts.advpng then
cmds += rep{"advpng -q ${opts} ${output}", opts=copts.advpng, output=output}
end
return cmds
end

18
pnpm-lock.yaml generated
View File

@@ -118,24 +118,6 @@ importers:
specifier: ^5.1.0
version: 5.1.0
tools/tupctime:
dependencies:
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
'@types/debug':
specifier: ^4.1.5
version: 4.1.13
'@types/node':
specifier: ^24.10.7
version: 24.13.3
better-sqlite3:
specifier: ^12.4.1
version: 12.11.1
debug:
specifier: ^4.1.1
version: 4.4.3
packages:
'@types/better-sqlite3@7.6.13':

View File

@@ -1,75 +0,0 @@
import fs from 'fs';
import pathlib from 'path';
import Database from 'better-sqlite3';
import debugfn from 'debug';
const debug = debugfn('tupctime');
const start = process.argv[2];
if (start !== undefined) {
process.chdir(start);
}
while (true) {
if (fs.existsSync('.tup')) {
break;
}
if (process.cwd() === '/') {
throw new Error("Can't find tup root.");
}
process.chdir('..');
}
const db = new Database('.tup/db');
const TUP_DB_FILE = 0;
const TUP_DB_DIR = 2;
const TUP_DB_GENERATED = 4;
const TUP_DB_GENERATED_DIR = 7;
// ^ and $ are virtual directories for environment variables
const traversalQuery = db.prepare<[number], {id: number, type: number, ctime: number, name: string}>(`
SELECT id, type, mtime as ctime, name
FROM node
WHERE dir = ?
AND type IN (${TUP_DB_FILE}, ${TUP_DB_DIR}, ${TUP_DB_GENERATED}, ${TUP_DB_GENERATED_DIR})
AND name NOT IN ('^', '$')`);
const updateQuery = db.prepare(`UPDATE node SET mtime = ? WHERE id = ?`);
function update() {
const stack : {isDir: boolean, id : number, path: string, ctime?: bigint, name?: string}[] = [{isDir: true, id: 0, path: "."}];
let item;
while (item = stack.pop()) {
if (!fs.existsSync(item.path)) {
debug(`Ignoring ${item.path}`);
continue;
}
if (item.isDir) {
for (const {id, type, ctime, name} of traversalQuery.iterate(item.id)) {
const isDir = [TUP_DB_DIR, TUP_DB_GENERATED_DIR].includes(type);
const path = pathlib.join(item.path, name);
stack.push({isDir, id, ctime: BigInt(ctime), path});
}
} else {
// bigint calculation is necessary to avoid rounding errors/spurious update
//
// typescript doesn't understand second argument to lstatSync, or
// that division can work between a number and bigint, so ignore
//
// @ts-ignore
const ctime = fs.lstatSync(item.path, {bigint: true}).ctimeMs / 1000n;
if (ctime !== item.ctime) {
debug(`Updating ${item.path}: ${ctime}, ${item.ctime}`);
if (updateQuery.run(ctime, item.id).changes !== 1)
throw new Error(`Couldn't update ${item.path}`);
}
}
}
}
db.transaction(update)();

View File

@@ -1,10 +0,0 @@
{
"dependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/debug": "^4.1.5",
"@types/node": "^24.10.7",
"better-sqlite3": "^12.4.1",
"debug": "^4.1.1"
},
"type": "module"
}

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig-base"
}

View File

@@ -3,7 +3,6 @@
"references": [
{"path": "./tsconfig.json"},
{"path": "tools/build"},
{"path": "tools/tupctime"},
{"path": "tools/deploy"},
{"path": "tools/sheet"},
{"path": "tools/smogdexspritesheet"},

View File

@@ -1,86 +0,0 @@
--
-- Strings
--
function trim(s)
return s:gsub("^%s*(.-)%s*$", "%1")
end
function rep(args)
local str = args[1]
local vars = args
local function fn(var)
local v = vars[var]
if v == nil then
error("unknown substitution: " .. var)
end
return v
end
-- Note: not equivalent to return str:gsub(...) due to multiple return values!
str = str:gsub("${(%a+)}", fn)
return str
end
--
-- Tables
--
-- Allow `for v in iter(table)` instead of `for _, v in ipairs(table)`
function iter(table)
local i = 1
return function ()
local v = table[i]
i = i + 1
return v
end
end
function astable(table)
local t = type(table)
if t == 'table' then
return table
elseif t == 'string' then
return {table}
elseif t == 'nil' then
return {}
end
end
-- Adapted from premake
function flatten(arr)
local result = {}
local function flatten(arr)
for v in iter(arr) do
if type(v) == "table" then
flatten(v)
else
table.insert(result, v)
end
end
end
flatten(arr)
return result
end
-- Adapted from https://stackoverflow.com/a/53038524
function filter(t, fn)
local j, n = 1, #t
for i=1,n do
if fn(t[i]) then
-- Move i's kept value to j's position, if it's not already there.
if (i ~= j) then
t[j] = t[i]
t[i] = nil
end
j = j + 1 -- Increment position of where we'll place the next kept value.
else
t[i] = nil
end
end
return t
end

View File

@@ -1,32 +0,0 @@
-- Similar to `parseFilename` in data/lib
function spritedata(basename)
local iter = basename:gmatch("[^-]+")
local result = {id = iter(), data = {}}
for flagtext in iter do
if flagtext:len() == 1 then
result.data[flagtext] = true
else
local flag = flagtext:sub(1, 1)
local text = flagtext:sub(2)
result.data[flag] = text
end
end
return result
end
function spriteglob(pat, flagspec)
local results = glob(pat)
local function fn(filename)
local sd = spritedata(tup.base(filename))
for k, v in pairs(flagspec or {}) do
-- Make sure both are booleans
if not not v ~= not not sd.data[k] then
return false
end
end
return true
end
filter(results, fn)
return results
end

View File

@@ -1,32 +0,0 @@
--
-- strict.lua
-- checks uses of undeclared global variables
-- All global variables must be 'declared' through a regular assignment
-- (even assigning nil will do) in a main chunk before being used
-- anywhere or assigned to inside a function.
--
-- NOTE: this file has been edited to remove mt.__newindex
local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
local mt = getmetatable(_G)
if mt == nil then
mt = {}
setmetatable(_G, mt)
end
mt.__declared = {}
local function what ()
local d = getinfo(3, "S")
return d and d.what or "C"
end
mt.__index = function (t, n)
if not mt.__declared[n] and what() ~= "C" then
error("variable '"..n.."' is not declared", 2)
end
return rawget(t, n)
end

View File

@@ -1,104 +0,0 @@
--
-- Configuration
--
-- getconfig returning an empty string on absence is inconvenient
function getconfig(str)
local v = tup.getconfig(str)
if v == "" then
return nil
else
return v
end
end
function booleanconfig(str)
local v = getconfig(str)
if v == nil or v == "false" then
return false
elseif v == "true" then
return true
else
error("boolean config must be true, false, or empty")
end
end
--
-- Globs
--
-- Workaround weird bug pre reported, where final path segment has two //
-- EDIT: AND workaround a bug Marty reported on Windows, where final path segment has a /\
local old_tup_glob = tup.glob
function tup.glob(pat)
local results = old_tup_glob(pat)
for i = 1, #results do
results[i] = results[i]:gsub("/\\", "/"):gsub("//", "/")
end
return results
end
-- A glob pattern is either an interpolated string, or a table of glob patterns
-- globpat_normalize("foo/*") --> {"foo/*"}
-- globpat_normalize({"foo/*"}) --> {"foo/*"}
local function globpat_normalize(pat)
if type(pat) == "string" then
return {pat}
elseif type(pat) == 'table' then
local result = {}
for x in iter(pat) do
result += globpat_normalize(x)
end
return result
else
error("bad globpat")
end
end
function glob(pat, opts)
local results = {}
for pat in iter(globpat_normalize(pat)) do
for file in iter(tup.glob(pat)) do
table.insert(results, file)
end
end
return results
end
-- Commands
local function cspec2cmd(cmdSpec)
local cmd = ""
for newcmd in iter(flatten(astable(cmdSpec))) do
newcmd = trim(newcmd)
if cmd ~= "" then
cmd = cmd .. " &&\n"
end
cmd = cmd .. newcmd
end
-- Abstract over tup's arcane display override
if cmdSpec.display then
cmd = rep{"^ ${display}^ ${cmd}", display=cmdSpec.display, cmd=cmd}
end
return cmd
end
local function do_rule(input, cspec, output, foreach)
local cmd = cspec2cmd(cspec)
if foreach then
return tup.foreach_rule(input, cmd, output)
else
return tup.rule(input, cmd, output)
end
end
function rule(input, cmd, output)
return do_rule(input, cmd, output, false)
end
function foreach_rule(input, cmd, output)
return do_rule(input, cmd, output, true)
end