diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 51c8d73..0e33a44 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -6,7 +6,7 @@ on: - cron: "10 0 * * *" # Runs at 00:10 UTC every day jobs: - release: + sync: name: Sync runs-on: docker container: @@ -20,53 +20,13 @@ jobs: with: token: ${{ secrets.githubtoken }} - - name: Install Luau - uses: https://github.com/EncodedVenom/install-luau@v4.3 - with: - version: "latest" - verbose: "true" - - - name: Install forgejo-cli + - name: Install fj run: | wget https://codeberg.org/Cyborus/forgejo-cli/releases/download/v0.2.0/forgejo-cli-linux.gz -O fj.gz gunzip fj.gz mv fj /usr/local/bin/fj chmod +x /usr/local/bin/fj - - name: Synchronize - run: lune run src/init -- sync jecs - - - name: Run Unit Tests - shell: bash - run: | - output=$(lune run src/init -- test jecs) - echo "$output" - if [[ "$output" == *"0 fails"* ]]; then - echo "Unit Tests Passed" - else - echo "Error: One or More Unit Tests Failed" - exit 1 - fi - - - name: Read Jecs Version - id: read_jecs_version - run: | - version=$(lune run src/read_version | tr '\n' ' ') - echo "JECS_VERSION=$version" >> $GITHUB_OUTPUT - - - name: Build With Rojo - run: | - cd jecs - rojo build default.project.json -o build.rbxm - mv build.rbxm ../jecs_nightly.rbxm - cd .. - - - name: Upload Build Artifact - uses: https://git.devmarked.win/actions/upload-artifact@v4 - with: - name: build - path: jecs_nightly.rbxm - - name: Authorize Pesde run: pesde auth login --token "${{ secrets.pesde_auth_token }}" @@ -76,8 +36,14 @@ jobs: wally login --token "${{ secrets.wally_auth_token }}" rm wally.toml - - name: Release - run: lune run src/init -- release jecs --pesde-scope marked/jecs_nightly --wally-scope mark-marks/jecs-nightly + - name: Sync & Release + run: lune run src/init + + - name: Read Jecs Version + id: read_jecs_version + run: | + version=$(lune run src/read_version | tr '\n' ' ') + echo "JECS_VERSION=$version" >> $GITHUB_OUTPUT - name: Create Pull Request id: create_pull_request diff --git a/jecs/build.txt b/jecs/build.txt deleted file mode 100644 index ff6901d..0000000 --- a/jecs/build.txt +++ /dev/null @@ -1,2 +0,0 @@ -modified = ["README.md", "LICENSE", ".luaurc", "CHANGELOG.md", "jecs.luau"] -version = "0.5.5-nightly.20250302T175658Z" diff --git a/jecs/default.project.json b/jecs/default.project.json index 5e66bad..d4531a0 100644 --- a/jecs/default.project.json +++ b/jecs/default.project.json @@ -1 +1,6 @@ -{"name":"jecs-nightly","tree":{"$path":"jecs.luau"}} \ No newline at end of file +{ + "name": "jecs", + "tree": { + "$path": "jecs.luau" + } +} diff --git a/jecs/pesde.toml b/jecs/pesde.toml index e33a380..909f63c 100644 --- a/jecs/pesde.toml +++ b/jecs/pesde.toml @@ -3,7 +3,7 @@ includes = ["init.luau", "pesde.toml", "README.md", "CHANGELOG.md", "LICENSE", " license = "MIT" name = "marked/jecs_nightly" repository = "https://git.devmarked.win/jecs-nightly" -version = "0.5.5-nightly.20250302T175658Z" +version = "0.5.5-nightly.20250302T042604Z" [indices] default = "https://github.com/daimond113/pesde-index" diff --git a/jecs/test.txt b/jecs/test.txt deleted file mode 100644 index 3669390..0000000 --- a/jecs/test.txt +++ /dev/null @@ -1,2 +0,0 @@ -passed = true -timestamp = "20250302T175659Z" diff --git a/jecs/test_fulllog.txt b/jecs/test_fulllog.txt deleted file mode 100644 index 1872afc..0000000 --- a/jecs/test_fulllog.txt +++ /dev/null @@ -1,114 +0,0 @@ -8.2 us  3 kBโ”‚ delete children of entity -9.4 us  2 kBโ”‚ remove friends of entity -328 ns  0  Bโ”‚ simple deletion of entity -removing -archetype -PASSโ”‚  - -world:cleanup() -PASSโ”‚  - -world:entity() -PASSโ”‚ unique IDs -PASSโ”‚ generations -PASSโ”‚ pairs -PASSโ”‚ Recycling -PASSโ”‚ Recycling max generation - -world:set() -PASSโ”‚ archetype move -PASSโ”‚ pairs - -world:remove() -PASSโ”‚ should allow remove a component that doesn't exist on entity - -world:add() -PASSโ”‚ idempotent -PASSโ”‚ archetype move - -world:query() -PASSโ”‚ cached -PASSโ”‚ multiple iter -PASSโ”‚ tag -PASSโ”‚ pairs -PASSโ”‚ query single component -PASSโ”‚ query missing component -PASSโ”‚ query more than 8 components -PASSโ”‚ should be able to get next results -PASSโ”‚ should query all matching entities when irrelevant component is removed -PASSโ”‚ should query all entities without B -PASSโ”‚ should allow querying for relations -PASSโ”‚ should allow wildcards in queries -PASSโ”‚ should match against multiple pairs -PASSโ”‚ should only relate alive entities -NONEโ”‚ should error when setting invalid pair -PASSโ”‚ should find target for ChildOf -PASSโ”‚ despawning while iterating -NONEโ”‚ iterator invalidation -SKIPโ”‚ adding -PASSโ”‚ spawning -PASSโ”‚ should not find any entities -PASSโ”‚ without - -world:each -PASSโ”‚  - -world:children -PASSโ”‚  - -world:clear() -PASSโ”‚ should remove its components -PASSโ”‚ should move last record - -world:has() -PASSโ”‚ should find Tag on entity -PASSโ”‚ should return false when missing one tag - -world:component() -PASSโ”‚ only components should have EcsComponent trait -PASSโ”‚ tag - -world:delete -PASSโ”‚ invoke OnRemove hooks -PASSโ”‚ delete recycled entity id used as component -PASSโ”‚ bug: Empty entity does not respect cleanup policy -PASSโ”‚ should allow deleting components -PASSโ”‚ delete entities using another Entity as component with Delete cleanup action -PASSโ”‚ delete children -PASSโ”‚ fast delete -PASSโ”‚ cycle - -world:target -PASSโ”‚ nth index -PASSโ”‚ infer index when unspecified -PASSโ”‚ loop until no target - -world:contains -PASSโ”‚  -PASSโ”‚ should not exist after delete - -Hooks -PASSโ”‚ OnAdd -PASSโ”‚ OnSet -PASSโ”‚ OnRemove - -change tracking -PASSโ”‚ #1 -PASSโ”‚ #2 - -repro -PASSโ”‚ #1 -PASSโ”‚ #2 - -wildcard query -PASSโ”‚ #1 -PASSโ”‚ #2 -PASSโ”‚ #3 - -world:delete() invokes OnRemove hook -PASSโ”‚ #1 -PASSโ”‚ #2 -PASSโ”‚ #3 - -68/68 test cases passed in 31.100 ms. -0 fails diff --git a/jecs/wally.toml b/jecs/wally.toml index 6151420..0f8c345 100644 --- a/jecs/wally.toml +++ b/jecs/wally.toml @@ -5,4 +5,4 @@ license = "MIT" name = "mark-marks/jecs-nightly" realm = "shared" registry = "https://github.com/UpliftGames/wally-index" -version = "0.5.5-nightly.20250302T175658Z" +version = "0.5.5-nightly.20250302T042604Z" diff --git a/jecs_nightly.rbxm b/jecs_nightly.rbxm deleted file mode 100644 index 6c0f21e..0000000 Binary files a/jecs_nightly.rbxm and /dev/null differ diff --git a/luau-install/binary.zip b/luau-install/binary.zip deleted file mode 100755 index 2390e23..0000000 Binary files a/luau-install/binary.zip and /dev/null differ diff --git a/luau-install/luau b/luau-install/luau deleted file mode 100755 index fa4fb72..0000000 Binary files a/luau-install/luau and /dev/null differ diff --git a/luau-install/luau-analyze b/luau-install/luau-analyze deleted file mode 100755 index 60e8fb3..0000000 Binary files a/luau-install/luau-analyze and /dev/null differ diff --git a/luau-install/luau-ast b/luau-install/luau-ast deleted file mode 100755 index af6dd09..0000000 Binary files a/luau-install/luau-ast and /dev/null differ diff --git a/luau-install/luau-compile b/luau-install/luau-compile deleted file mode 100755 index fd4ed9f..0000000 Binary files a/luau-install/luau-compile and /dev/null differ diff --git a/rokit.toml b/rokit.toml index cf02dbc..1cc79c6 100644 --- a/rokit.toml +++ b/rokit.toml @@ -9,4 +9,3 @@ luau-lsp = "johnnymorganz/luau-lsp@1.37.0" # https://discord.com/channels/385151 stylua = "johnnymorganz/stylua@2.0.2" wally = "upliftgames/wally@0.3.2" pesde = "pesde-pkg/pesde@0.6.0+registry.0.2.0" -rojo = "rojo-rbx/rojo@7.4.4" diff --git a/src/init.luau b/src/init.luau index 299242a..2cd1046 100644 --- a/src/init.luau +++ b/src/init.luau @@ -1,80 +1,16 @@ --!strict -local fs = require("@lune/fs") -local process = require("@lune/process") - -local frkcli = require("./util/frkcli") local release = require("./release") local sync = require("./sync") -local test = require("./test") -local cli = frkcli.new_subcommands("nightly_cli", "A CLI to sync & release jecs nightly") - -local command_sync = cli:add_subcommand("sync", "Synchronize files from the jecs repository") -command_sync:add_positional( - "to", - { help = "Directory to synchronize the files in, defaults to current working directory", default = process.cwd } -) - -local command_release = cli:add_subcommand("release", "Publish a release from the given directory") -command_release:add_positional( - "from", - { help = "Directory to release from, defaults to current working directory", default = process.cwd } -) -command_release:add_flag("dry", { help = "Dry run the publishes" }) -command_release:add_option( - "pesde-scope", - { help = "The pesde scope to publish under (eg. marked/jecs_nightly)", default = "nil" } -) -command_release:add_option( - "wally-scope", - { help = "The wally scope to publish under (eg. mark-marks/jecs-nightly)", default = "nil" } -) - -local command_test = cli:add_subcommand("test", "Run unit tests on the stored jecs source") -command_test:add_positional("in", { - help = "Directory with the jecs source to run tests in, defaults to current working directory", - default = process.cwd, -}) - -local parsed, err = cli:parse(process.args) -if err ~= nil then - error(err) +local res = sync("jecs") +if not res.ok then + print(`Can't continue: {res.err}`) + return end -assert(parsed ~= nil) -local values = parsed.result.values -local flags = parsed.result.flags -if parsed.command == "sync" then - sync(values.to) -elseif parsed.command == "release" then - local scopes = {} - if values["pesde-scope"] ~= "nil" then - scopes.pesde = values["pesde-scope"] - end - if values["wally-scope"] ~= "nil" then - scopes.wally = values["wally-scope"] - end - - local metadata = fs.metadata(values.from) - if not metadata.exists or metadata.kind ~= "dir" then - error(`The path {values.from} doesn't exist or isn't a valid directory.`) - end - - local fpath = process.cwd - if values.from ~= process.cwd then - fpath ..= values.from - end - - if flags.dry then - release(fpath, scopes, true) - else - release(fpath, scopes, false) - end -elseif parsed.command == "test" then - local fpath = process.cwd - if values["in"] ~= process.cwd then - fpath ..= values["in"] - end - - test(fpath) +if res.val == false then + print(`No changes made. Aborting.`) + return end + +release("jecs", { pesde = "marked/jecs_nightly", wally = "mark-marks/jecs-nightly" }, false) diff --git a/src/release.luau b/src/release.luau index 7fa6190..7499d5d 100644 --- a/src/release.luau +++ b/src/release.luau @@ -1,14 +1,20 @@ --!strict +local datetime = require("@lune/datetime") local fs = require("@lune/fs") +local net = require("@lune/net") local process = require("@lune/process") local serde = require("@lune/serde") local stdio = require("@lune/stdio") local progress_bar = require("./util/progress") local result = require("./util/result") -local shared = require("./shared") local types = require("./types") +-- Returns an ISO 8601 date (YYYYmmddThhmmssZ) +local function iso_date_light(now: datetime.DateTime): string + return now:formatUniversalTime("%Y%m%dT%H%M%SZ") +end + local function make_pesde_manifest(version: string, scope: string): types.PesdeManifest return { name = scope, @@ -57,17 +63,32 @@ local function make_wally_manifest(version: string, scope: string): types.WallyM } end -local function release(origin: string, scopes: { wally: string?, pesde: string? }, dry: boolean?): result.Identity +local function round_to(n: number, places: number) + local x = 10 ^ (places or 0) + return math.round(n * x) / x +end + +--- Fetches the given file raw from the jecs github +local function fetch_raw(file: string): result.Identity + local res = net.request(`https://raw.githubusercontent.com/Ukendio/jecs/refs/heads/main/{file}`) + if not res.ok then + return result(false, `Not ok: {res.statusMessage} @ {res.statusCode}`) + end + return result(true, res.body) +end + +local function release(origin: string, scopes: { wally: string, pesde: string }, dry: boolean?): result.Identity local begin = os.clock() + local now = datetime.now() local progress = progress_bar .new() :withStage("init", "Initializing") - :withStage("version", "Pulling version from build metadata") + :withStage("pull", "Pull latest version") :withStage("prepare", "Preparing manifests") :withStage("release (pesde)", "Releasing on pesde") :withStage("release (wally)", "Releasing on wally") - progress:start() -- init + progress:start() if dry == nil then dry = true @@ -79,96 +100,62 @@ local function release(origin: string, scopes: { wally: string?, pesde: string? return result(false, `{origin} is not a valid directory which exists.`) end - progress:nextStage() -- version + progress:nextStage() - local version - do - local encoded_metadata = fs.readFile(`{origin}/build.txt`) - local metadata: { version: string } = serde.decode("toml", encoded_metadata) - version = metadata.version + local wally_contents = fetch_raw("wally.toml") + if not wally_contents.ok then + progress:stop() + stdio.ewrite(`๐Ÿ”ฅ Couldn't get the jecs wally manifest:\n{wally_contents.err}\n`) + return result(false, `Couldn't get the jecs wally manifest:\n{wally_contents.err}`) end - progress:nextStage() -- prepare + local parsed: types.WallyManifest = serde.decode("toml", wally_contents.val) + local version = `{parsed.package.version}-nightly.{iso_date_light(now)}` - if scopes.pesde then + progress:nextStage() + + do local manifest = make_pesde_manifest(version, scopes.pesde) local encoded = serde.encode("toml", manifest) fs.writeFile(`{origin}/pesde.toml`, encoded) end - if scopes.wally then + do local manifest = make_wally_manifest(version, scopes.wally) local encoded = serde.encode("toml", manifest) fs.writeFile(`{origin}/wally.toml`, encoded) end - progress:nextStage() -- release (pesde) + progress:nextStage() - local cwd = origin --process.cwd .. origin + local cwd = process.cwd .. origin - local res_pesde - if scopes.pesde then - if not dry then - res_pesde = process.spawn("pesde", { "publish", "-y" }, { cwd = cwd }) - else - res_pesde = process.spawn("pesde", { "publish", "-d", "-y" }, { cwd = cwd }) - end - - if not res_pesde.ok then - progress:stop() - print(`-- Pesde error:\nstdout\n{res_pesde.stdout}\nstderr\n{res_pesde.stderr}`) - return result(false, `Pesde error:\nstdout\n{res_pesde.stdout}\nstderr\n{res_pesde.stderr}`) - end + if not dry then + process.spawn("pesde", { "publish", "-y" }, { cwd = cwd }) + else + process.spawn("pesde", { "publish", "-d", "-y" }, { cwd = cwd }) end - progress:nextStage() -- release (wally) + progress:nextStage() - local res_wally - if scopes.wally then - if not dry then - res_wally = process.spawn("wally", { "publish" }, { cwd = cwd }) - else - res_wally = process.spawn("wally", { "package", "--output", "wally.tar.gz" }, { cwd = cwd }) - end - - if not res_wally.ok then - progress:stop() - print(`-- Wally error:\nstdout\n{res_wally.stdout}\nstderr\n{res_wally.stderr}`) - return result(false, `Wally error:\nstdout\n{res_wally.stdout}\nstderr\n{res_wally.stderr}`) - end + if not dry then + process.spawn("wally", { "publish" }, { cwd = cwd }) + else + process.spawn("wally", { "package", "--output", "wally.tar.gz" }, { cwd = cwd }) end - progress:stop() -- finish + progress:stop() - if res_pesde then - print(`-- Pesde out:\nstdout\n{res_pesde.stdout}\nstderr\n{res_pesde.stderr}`) - end - - if res_wally then - print(`-- Wally out:\nstdout\n{res_wally.stdout}\nstderr\n{res_wally.stderr}`) - end - - local took = shared.round_to((os.clock() - begin) * 1_000, 2) + local took = round_to((os.clock() - begin) * 1_000, 2) if not dry then print(`๐Ÿš€ Published packages {stdio.style("dim")}(took {took}ms){stdio.style("reset")}`) else print(`๐Ÿ“ฆ Packaged packages {stdio.style("dim")}(took {took}ms){stdio.style("reset")}`) end - - if scopes.pesde and scopes.wally then - print({ - pesde = `{scopes.pesde}@{version}`, - wally = `{scopes.wally}@{version}`, - }) - elseif scopes.pesde then - print({ - pesde = `{scopes.pesde}@{version}`, - }) - elseif scopes.wally then - print({ - wally = `{scopes.wally}@{version}`, - }) - end + print({ + pesde = `{scopes.pesde}@{version}`, + wally = `{scopes.wally}@{version}`, + }) return result(true, nil) end diff --git a/src/shared.luau b/src/shared.luau deleted file mode 100644 index 5194e9a..0000000 --- a/src/shared.luau +++ /dev/null @@ -1,47 +0,0 @@ ---!strict -local datetime = require("@lune/datetime") -local fs = require("@lune/fs") -local net = require("@lune/net") - -local result = require("./util/result") - ---- Fetches the given file raw from the jecs github -local function fetch_raw(file: string): result.Identity - local res = net.request(`https://raw.githubusercontent.com/Ukendio/jecs/refs/heads/main/{file}`) - if not res.ok then - return result(false, `Not ok: {res.statusMessage} @ {res.statusCode}`) - end - return result(true, res.body) -end - -local function save_if_diff(filepath: string, contents: string): result.Identity - if not fs.metadata(filepath).exists then - fs.writeFile(filepath, contents) - return result(true, nil) - end - - local existing = fs.readFile(filepath) - if existing == contents then - return result(false, "Contents are the same.") - end - - fs.writeFile(filepath, contents) - return result(true, nil) -end - -local function round_to(n: number, places: number) - local x = 10 ^ (places or 0) - return math.round(n * x) / x -end - --- Returns an ISO 8601 date (YYYYmmddThhmmssZ) -local function iso_date_light(date: datetime.DateTime): string - return date:formatUniversalTime("%Y%m%dT%H%M%SZ") -end - -return { - fetch_raw = fetch_raw, - save_if_diff = save_if_diff, - round_to = round_to, - iso_date_light = iso_date_light, -} diff --git a/src/sync.luau b/src/sync.luau index 2407cb4..1fd4252 100644 --- a/src/sync.luau +++ b/src/sync.luau @@ -1,17 +1,42 @@ --!strict -local datetime = require("@lune/datetime") local fs = require("@lune/fs") -local serde = require("@lune/serde") +local net = require("@lune/net") local stdio = require("@lune/stdio") local progress_bar = require("./util/progress") local result = require("./util/result") -local shared = require("./shared") -local types = require("./types") + +--- Fetches the given file raw from the jecs github +local function fetch_raw(file: string): result.Identity + local res = net.request(`https://raw.githubusercontent.com/Ukendio/jecs/refs/heads/main/{file}`) + if not res.ok then + return result(false, `Not ok: {res.statusMessage} @ {res.statusCode}`) + end + return result(true, res.body) +end + +local function save_if_diff(filepath: string, contents: string): result.Identity + if not fs.metadata(filepath).exists then + fs.writeFile(filepath, contents) + return result(true, nil) + end + + local existing = fs.readFile(filepath) + if existing == contents then + return result(false, "Contents are the same.") + end + + fs.writeFile(filepath, contents) + return result(true, nil) +end + +local function round_to(n: number, places: number) + local x = 10 ^ (places or 0) + return math.round(n * x) / x +end --- Synchronizes the required files from the jecs main branch. local function sync(to: string): result.Identity - local now = datetime.now() local begin = os.clock() local progress = progress_bar @@ -19,15 +44,13 @@ local function sync(to: string): result.Identity :withStage("init", "Initializing") :withStage("fetch", "Fetching latest files") :withStage("save", "Saving files") - :withStage("version", "Pulling latest jecs version") - :withStage("metadata", "Writing build metadata") - progress:start() -- init + progress:start() if not fs.metadata(to).exists then fs.writeDir(to) end - progress:nextStage() -- fetch + progress:nextStage() local includes = { "jecs.luau", @@ -35,12 +58,13 @@ local function sync(to: string): result.Identity "CHANGELOG.md", "LICENSE", ".luaurc", + "default.project.json", } local sources = {} for _, file in includes do - local contents = shared.fetch_raw(file) + local contents = fetch_raw(file) if not contents.ok then progress:stop() stdio.ewrite(`๐Ÿ”ฅ Couldn't get the latest source for {file}:\n{contents.err}\n`) @@ -50,12 +74,12 @@ local function sync(to: string): result.Identity sources[file] = contents.val end - progress:nextStage() -- save + progress:nextStage() local sources_modified = {} local any_changed = false for file, contents in sources do - local res = shared.save_if_diff(`{to}/{file}`, contents) + local res = save_if_diff(`{to}/{file}`, contents) if res.ok then any_changed = true table.insert(sources_modified, file) @@ -64,7 +88,7 @@ local function sync(to: string): result.Identity if not any_changed then progress:stop() - local took = shared.round_to((os.clock() - begin) * 1_000, 2) + local took = round_to((os.clock() - begin) * 1_000, 2) print( `๐Ÿ•› Finished synchronizing, no changes since latest source {stdio.style("dim")}(took {took}ms){stdio.style( "reset" @@ -73,39 +97,9 @@ local function sync(to: string): result.Identity return result(true, false) end - local project_json = { - name = "jecs-nightly", - tree = { - ["$path"] = "jecs.luau", - }, - } - local encoded_project_json = serde.encode("json", project_json) - fs.writeFile(`{to}/default.project.json`, encoded_project_json) + progress:stop() - progress:nextStage() -- version - - local wally_contents = shared.fetch_raw("wally.toml") - if not wally_contents.ok then - progress:stop() - stdio.ewrite(`๐Ÿ”ฅ Couldn't get the jecs wally manifest:\n{wally_contents.err}\n`) - return result(false, `Couldn't get the jecs wally manifest:\n{wally_contents.err}`) - end - - local parsed: types.WallyManifest = serde.decode("toml", wally_contents.val) - - progress:nextStage() -- metadata - - local version = `{parsed.package.version}-nightly.{shared.iso_date_light(now)}` - local metadata = { - version = version, - modified = sources_modified, - } - local encoded_metadata = serde.encode("toml", metadata) - fs.writeFile(`{to}/build.txt`, encoded_metadata) - - progress:stop() -- finish - - local took = shared.round_to((os.clock() - begin) * 1_000, 2) + local took = round_to((os.clock() - begin) * 1_000, 2) print(`๐Ÿชจ Finished synchronizing {stdio.style("dim")}(took {took}ms){stdio.style("reset")}`) print(`Changed files:`) print(sources_modified) diff --git a/src/test.luau b/src/test.luau deleted file mode 100644 index c12686d..0000000 --- a/src/test.luau +++ /dev/null @@ -1,105 +0,0 @@ ---!strict -local datetime = require("@lune/datetime") -local fs = require("@lune/fs") -local process = require("@lune/process") -local serde = require("@lune/serde") -local stdio = require("@lune/stdio") - -local progress_bar = require("./util/progress") -local result = require("./util/result") -local shared = require("./shared") - -local function test(origin: string): result.Identity - local now = datetime.now() - local begin = os.clock() - - local progress = progress_bar - .new() - :withStage("init", "Initializing") - :withStage("fetch", "Fetching latest tests") - :withStage("test", "Running tests") - :withStage("cleanup", "Cleaning up") - :withStage("metadata", "Writing test metadata") - progress:start() -- init - - if not fs.metadata(origin).exists then - progress:stop() - stdio.ewrite(`๐Ÿ”ฅ {origin} is not a valid directory which exists.\n`) - return result(false, `{origin} is not a valid directory which exists.`) - end - - if not fs.metadata(`{origin}/jecs.luau`).exists then - progress:stop() - stdio.ewrite(`๐Ÿ”ฅ {origin}/jecs.luau is not a vald file which exists.\n`) - return result(false, `{origin}/jecs.luau is not a vald file which exists.`) - end - - if fs.metadata(`{origin}/test`).exists then - fs.removeDir(`{origin}/test`) - end - fs.writeDir(`{origin}/test`) - - progress:nextStage() -- fetch - - do - local contents = shared.fetch_raw("test/testkit.luau") - if not contents.ok then - progress:stop() - stdio.ewrite(`๐Ÿ”ฅ Couldn't get the latest source for test/testkit.luau:\n{contents.err}\n`) - return result(false, "Couldn't get the latest source for test/testkit.luau.") - end - fs.writeFile(`{origin}/test/testkit.luau`, contents.val) - end - do - local contents = shared.fetch_raw("test/tests.luau") - if not contents.ok then - progress:stop() - stdio.ewrite(`๐Ÿ”ฅ Couldn't get the latest source for test/tests.luau:\n{contents.err}\n`) - return result(false, "Couldn't get the latest source for test/tests.luau.") - end - fs.writeFile(`{origin}/test/tests.luau`, contents.val) - end - - progress:nextStage() -- test - - local test_result = process.spawn("luau", { "test/tests.luau" }, { cwd = origin }) - if not test_result.ok then - progress:stop() - stdio.ewrite(`๐Ÿ”ฅ Tests failed to run:\n{test_result.stderr}\n`) - return result(false, `Tests failed to run.`) - end - - local passed = true - if not string.find(test_result.stdout, "0 fails") then - passed = false - end - - progress:nextStage() -- cleanup - - fs.removeDir(`{origin}/test`) - - progress:nextStage() -- metadata - - local metadata = { - timestamp = shared.iso_date_light(now), - passed = passed, - } - local encoded_metadata = serde.encode("toml", metadata) - fs.writeFile(`{origin}/test.txt`, encoded_metadata) - fs.writeFile(`{origin}/test_fulllog.txt`, test_result.stdout) - - progress:stop() -- finish - - print(test_result.stdout) - - local passed_txt = if passed - then `{stdio.style("bold")}{stdio.color("green")}passed{stdio.color("reset")}{stdio.style("reset")}` - else `{stdio.style("bold")}{stdio.color("red")}failed{stdio.color("reset")}{stdio.style("reset")}` - - local took = shared.round_to((os.clock() - begin) * 1_000, 2) - print(`๐Ÿงช Finished testing, {passed_txt} {stdio.style("dim")}(took {took}ms){stdio.style("reset")}`) - - return result(true, true) -end - -return test diff --git a/src/util/frkcli.luau b/src/util/frkcli.luau deleted file mode 100644 index d306791..0000000 --- a/src/util/frkcli.luau +++ /dev/null @@ -1,586 +0,0 @@ ---!nolint LocalShadow --- https://raw.githubusercontent.com/itsfrank/frkcli/refs/heads/main/src/frkcli.luau - -local process = require("@lune/process") - -local M = {} - -type ArgKind = "POSITIONAL" | "FLAG" | "OPTION" -type ArgOptions = { - help: string?, - aliases: { string }?, - default: string?, -- if this is not nil, the arg will be optional -} - --- this is what is stored, we guarante non nullity when args are added so this types makes Luau feel better -type ArgOptionsSafe = { - help: string, - aliases: { string }, - default: string?, -} - -type ArgData = { - name: string, - kind: ArgKind, - options: ArgOptionsSafe, -} - -type ParseResult = { - values: { [string]: string }, - flags: { [string]: boolean }, - fwd_args: { string }, -- all args after `--` -} - -type SubcommandParseResult = { - command: string, - result: ParseResult, -} - -local DEFAULT_OPTIONS: ArgOptionsSafe = { - help = "", - default = nil, - aliases = {}, -} - -local DEFAULT_helpset = { - ["-h"] = true, - ["--help"] = true, -} - -local function validate_key_or_error(key: string) - if (key:sub(1, 2) == "--" or key:sub(1, 1) == "-") and not key:find(" ") then - return - end - error(`arg key {key} is invalid. Keys must start with either '-' or '--' and may not contain spaces`) -end - -local function validate_subcommand_name_or_error(name: string) - if name:sub(1, 2) == "--" or name:sub(1, 1) == "-" or name:find(" ") then - error(`subcommand name '{name}' is invalid. Name must not start with '-' or '--' and may not contain spaces`) - end -end - -type HelpSection = { title: string, lines: { { string } | string }? } -local function make_help(sections: { HelpSection }, indent: number?): string - local function align_cols(rows: { { string } }, sep: string?): { { string } } - local sep = if sep == nil then " " else sep - local max_col_lengths: { number } = {} - for _, row in rows do - for i, s in row do - if max_col_lengths[i] == nil or s:len() > max_col_lengths[i] then - max_col_lengths[i] = s:len() - end - end - end - local aligned_rows: { { string } } = {} - for _, row in rows do - local line: { string } = {} - for i, col in row do - table.insert(line, col) - if i < #row then - local spacing = sep - local diff = max_col_lengths[i] - col:len() - if diff > 0 then - spacing = string.rep(" ", diff) .. sep - end - table.insert(line, spacing) - end - end - table.insert(aligned_rows, line) - end - return aligned_rows - end - - local function append_list(dest: { T }, src: { T }) - for _, v in src do - table.insert(dest, v) - end - end - - local ind = " " - if indent ~= nil then - ind = string.rep(" ", indent) - end - - local help_lines: { { string } } = {} - - for _, s in sections do - table.insert(help_lines, { s.title }) - if s.lines ~= nil then - local section_lines: { { string } } = {} - for _, l in s.lines do - if typeof(l) == "string" then - table.insert(section_lines, { ind, l }) - else - table.insert(section_lines, { ind, table.unpack(l) }) - end - end - append_list(help_lines, align_cols(section_lines)) - end - table.insert(help_lines, {}) - end - - -- remove last empty line - table.remove(help_lines) - - local help_text = "" - for _, line in help_lines do - help_text ..= table.concat(line, " ") .. "\n" - end - return help_text -end --- subcommand cli, must be first positional arg -function M.new_subcommands(name, description: string?, helpkeys: { string }?) - -- args that trigger print help + abort - local helpset: { [string]: boolean } = DEFAULT_helpset - if helpkeys ~= nil then - helpset = {} - for _, k in helpkeys do - helpset[k] = true - end - end - - local cli = {} - cli.name = name - cli.description = description - cli._helpkeys = helpkeys - cli._helpset = helpset - cli._subcommands = {} - - function cli:add_subcommand(name: string, description: string?) - validate_subcommand_name_or_error(name) - local sc = M.new(name, description, cli._helpkeys) - cli._subcommands[name] = sc - return sc - end - - function cli:parse(args: { string }): (SubcommandParseResult?, string?) - if #args == 0 then - return nil, "insufficient arguments, required at least 1, got 0" - end - local command = args[1] - assert(command ~= nil) - - -- asking for help? - if cli._helpset[command] ~= nil then - print(cli:help()) - process.exit(0) - end - - local subcommand = cli._subcommands[command] - if subcommand == nil then - return nil, `'{command} is not a valid subcommand` - end - - local sub_args = { table.unpack(args, 2) } - local res, err = subcommand:parse(sub_args) - if err ~= nil then - return nil, err - end - assert(res ~= nil) - - return { - command = command, - result = res, - }, nil - end - - function cli:help(indent: number?): string - local help_sections: { HelpSection } = {} - - table.insert(help_sections, { title = `usage: {cli.name} ` }) - - if cli.description ~= nil then - table.insert(help_sections, { - title = "description:", - lines = { { cli.description } }, - }) - end - - local subcommand_lines = {} - for _, c in cli._subcommands do - local desc = if c.description then c.description else "" - table.insert(subcommand_lines, { c.name, desc }) - end - - table.insert(help_sections, { - title = "commands:", - lines = subcommand_lines, - }) - - return make_help(help_sections, indent) - end - - return cli -end - -function M.new(name: string, description: string?, helpkeys: { string }?) - -- args that trigger print help + abort - local helpset: { [string]: boolean } = DEFAULT_helpset - if helpkeys ~= nil then - helpset = {} - for _, k in helpkeys do - helpset[k] = true - end - end - - -- I'm fairly certain this is not the right pattern for making objects, but it results in single definition + great LSP so I'm happy - local cli = {} - cli.name = name - cli.description = description - cli._helpset = helpset - cli._positionals = {} :: { ArgData } - cli._flags = {} :: { ArgData } - cli._options = {} :: { ArgData } - cli._argdata_set = {} :: { [string]: ArgData } - cli._required_list = {} :: { string } - cli._lookup = {} :: { [string]: ArgData } - cli._required_positional_count = 0 - cli._default_result = { values = {}, flags = {} } :: ParseResult - - local function add_arg_lookups(keys: { string }, arg_data: ArgData) - for _, k in keys do - if cli._helpset[k] ~= nil then - error(`key '{k}' is already used as a help key.`) - end - validate_key_or_error(k) - if cli._lookup[k] ~= nil then - error(`key '{k}' already exists.`) - end - cli._lookup[k] = arg_data - end - end - - local function add_name_error_on_duplicate(name: string, data: ArgData) - if cli._argdata_set[name] ~= nil then - error(`arg with name {name} already exists`) - end - cli._argdata_set[name] = data - end - - local function make_safe_options(options: ArgOptions?): ArgOptionsSafe - if options == nil then - return DEFAULT_OPTIONS - end - assert(options) - - options.aliases = if options.aliases == nil then {} else options.aliases - - return options :: ArgOptionsSafe - end - - -- return is guaranteed to start with at least one '-', if none, adds '--' - local function to_lookup_name(name: string): string - if name:sub(1, 1) == "-" then - return name - end - return "--" .. name - end - - -- return is guaranted to not start with '-' - local function to_result_name(name: string): string - while name:sub(1, 1) == "-" do - name = name:sub(2, -1) - end - return name - end - - function cli:add_positional(name: string, options: ArgOptions?) - if name:sub(1, 1) == "-" then - error(`invalid arg name '{name}', positional cannot start with dashes`) - end - - local options = make_safe_options(options) - - local arg_data: ArgData = { - name = name, - kind = "POSITIONAL", - options = options, - } - add_name_error_on_duplicate(name, arg_data) - if options.default == nil then - -- check positional required ordering, can't have a required pos after an optional one - local last = cli._positionals[#cli._positionals] - if last ~= nil and last.options.default ~= nil then - error( - `{name} is required, but {last.name} is optional. Cannot have required positional after optional positional` - ) - end - - table.insert(cli._required_list, name) - else - cli._default_result.values[name] = options.default - end - table.insert(cli._positionals, arg_data) - end - - function cli:add_flag(name: string, options: ArgOptions?) - if options and options.default ~= nil then - -- these are called constants :) - error(`flag {name} has non nil default, default value is not supported for flags`) - end - - local options = make_safe_options(options) - - local lookup_name = to_lookup_name(name) - name = to_result_name(name) - for i, v in options.aliases do - options.aliases[i] = to_lookup_name(v) - end - - local arg_data: ArgData = { - name = name, - kind = "FLAG", - options = options, - } - add_name_error_on_duplicate(name, arg_data) - add_arg_lookups({ lookup_name, table.unpack(options.aliases) }, arg_data) - - if options.default ~= nil then - error(`flag {name} has non nil default value, defaults are not supported for flag args`) - end - - cli._default_result.flags[name] = false - table.insert(cli._flags, arg_data) - end - - function cli:add_option(name: string, options: ArgOptions?) - local options = make_safe_options(options) - - local lookup_name = to_lookup_name(name) - name = to_result_name(name) - for i, v in options.aliases do - options.aliases[i] = to_lookup_name(v) - end - - local arg_data: ArgData = { - name = name, - kind = "OPTION", - options = options, - } - add_name_error_on_duplicate(name, arg_data) - add_arg_lookups({ lookup_name, table.unpack(options.aliases) }, arg_data) - if options.default == nil then - table.insert(cli._required_list, name) - else - if options.default == nil then - error(`optional arg {name} must have a default value`) - end - assert(options.default ~= nil) - cli._default_result.values[name] = options.default - end - table.insert(cli._options, arg_data) - end - - -- return: data, err - where data is a table, and err is a string - -- errors early, first encountered error ends the parse - -- if a help key is found will print help text and exit the process - function cli:parse(args: { string }): (ParseResult?, string?) - local parsed: ParseResult = { values = {}, flags = {}, fwd_args = {} } :: ParseResult - - local positional_idx = 1 - local skip_next = false -- used for options - for i, arg in args do - -- asking for help? - if cli._helpset[arg] ~= nil then - print(cli:help()) - process.exit(0) - end - - if skip_next then - skip_next = false - continue - end - - -- rest of args should be fwd args - if arg == "--" then - parsed.fwd_args = { table.unpack(args, i + 1) } - break - end - - local has_dash = arg:sub(1, 1) == "-" - local arg_data: ArgData? = nil - local value: string? = nil - - if not has_dash then -- positional - if positional_idx > #cli._positionals then - return nil, `too many positional arguments, expected {#cli._positionals}.` - end - arg_data = cli._positionals[positional_idx] - positional_idx += 1 - value = arg - else -- flag or option - arg_data = cli._lookup[arg] - end - - if arg_data == nil then - return nil, `unknown flag or option: '{arg}'` - end - assert(arg_data ~= nil) - - -- handle flags early - if arg_data.kind == "FLAG" then - parsed.flags[arg_data.name] = true - continue - end - - -- get option value - if arg_data.kind == "OPTION" then - if i < #args then - value = args[i + 1] - skip_next = true - end - end - - -- resolve positionals and options - if value == nil then - return nil, `no value provided for option '{arg}'` - end - assert(value ~= nil) - - parsed.values[arg_data.name] = value - end - - -- check all required options are provided - for _, name in cli._required_list do - if parsed.values[name] == nil and parsed.flags[name] == nil then - local kind = cli._argdata_set[name].kind:lower() - return nil, `required {kind} arg '{name}' was not found` - end - end - - -- apply defaults for missing optional - for k, v in cli._default_result.flags do - if parsed.flags[k] == nil then - parsed.flags[k] = v - end - end - for k, v in cli._default_result.values do - if parsed.values[k] == nil then - parsed.values[k] = v - end - end - - return parsed, nil - end - - function cli:help(indent: number?): string - local function align_cols(rows: { { string } }, sep: string?): { { string } } - local sep = if sep == nil then " " else sep - local max_col_lengths: { number } = {} - for _, row in rows do - for i, s in row do - if max_col_lengths[i] == nil or s:len() > max_col_lengths[i] then - max_col_lengths[i] = s:len() - end - end - end - local aligned_rows: { { string } } = {} - for _, row in rows do - local line: { string } = {} - for i, col in row do - table.insert(line, col) - if i < #row then - local spacing = sep - local diff = max_col_lengths[i] - col:len() - if diff > 0 then - spacing = string.rep(" ", diff) .. sep - end - table.insert(line, spacing) - end - end - table.insert(aligned_rows, line) - end - return aligned_rows - end - - local help_sections: { HelpSection } = {} - - -- usage - local usage = `usage: {cli.name}` - if #cli._flags > 0 or #cli._options > 0 then - usage ..= " [options]" - end - for _, arg in cli._positionals do - if arg.options.default == nil then - usage ..= ` <{arg.name}>` - else - usage ..= ` [{arg.name}]` - end - end - - table.insert(help_sections, { title = usage }) - - -- description - if cli.description ~= nil then - table.insert(help_sections, { title = "description:", lines = { cli.description } }) - end - - local function make_arg_line(arg: ArgData): { string } - local keys = arg.name - if arg.kind ~= "POSITIONAL" then - keys = `--{arg.name}` - for _, a in arg.options.aliases do - keys ..= `, {a}` - end - end - - local reqdef = "" - -- optional & default - if arg.kind ~= "FLAG" then - if arg.options.default == nil then - reqdef ..= "[required]" - else - assert(arg.options.default) - reqdef ..= `[default: '{arg.options.default}']` - end - end - - local help = "" - if arg.options.help then - if help ~= "" then - help ..= " " - end - help ..= arg.options.help - end - - return { keys, reqdef, help } - end - - -- positionals - if #cli._positionals > 0 then - local positional_lines: { { string } } = {} - for _, arg in cli._positionals do - table.insert(positional_lines, make_arg_line(arg)) - end - table.insert(help_sections, { title = "positional arguments:", lines = positional_lines }) - end - - -- flags - if #cli._flags > 0 then - local flag_lines: { { string } } = {} - for _, arg in cli._flags do - table.insert(flag_lines, make_arg_line(arg)) - end - table.insert(help_sections, { title = "flags:", lines = flag_lines }) - end - - -- options - if #cli._options > 0 then - local option_lines: { { string } } = {} - for _, arg in cli._options do - table.insert(option_lines, make_arg_line(arg)) - end - table.insert(help_sections, { title = "options:", lines = option_lines }) - end - - return make_help(help_sections, indent) - end - - return cli -end - -export type Cli = typeof(M.new("")) -export type CliSubcommands = typeof(M.new_subcommands("")) - -return M