818acd921e18
close the phase 7a review: read only a quoted literal as a version, refuse an empty artifact, take stray build children with it, and prove koh builds itself in a test (3.0.80)
- time
- 2026-09-27 10:19 UTC · 2 weeks ago
- machine
- Junes-MacBook-Pro.local
- agent
- deepseek/deepseek-flash
- session
- koh 3 phases 6-8
- parent
- ba2f1a3bcb27
↓ koh-818acd92.tar.gzatlas at this save
files changed6 files
VERSION+1 −1src/build/build.v+10 −1src/build/build_test.v+38 −2src/build/version.v+19 −15tests/cli_test.v+23 −0v.mod+1 −1src/build/build.v+10 −1
@@ -272,10 +272,14 @@272272 if os.is_dir(full) {273273 return error('${path} is a folder — a release file is one file')274274 }275+ size := os.file_size(full)276+ if size == 0 {277+ return error('the ${platform} build produced an empty ${path}')278+ }275279 return Built{276280 platform: platform277281 path: path278- size: os.file_size(full)282+ size: size279283 }280284 }281285 @@ -318,9 +322,14 @@318322 }319323 timed_out := child.is_alive()320324 if timed_out {325+ // Kill first: waiting on a hung build waits forever.321326 child.signal_pgkill()322327 child.wait()323328 }329+ // Whatever the command left running goes with it: a background child could330+ // still be writing the artifact after it was hashed. Its own process group331+ // again, so a stray child dies and nothing else does.332+ child.signal_pgkill()324333 stdout := os.read_file(stdout_path) or { '' }325334 stderr := os.read_file(stderr_path) or { '' }326335 return Run{src/build/build_test.v+38 −2
@@ -107,14 +107,31 @@107107 write(root, 'build.gradle', 'versionName "2.17.43"\n')!108108 assert project_version(root)! == '2.17.43'109109 110- // A commented-out literal is not read, and versionNameSuffix is not a111- // versionName: with nothing left, the refusal names a VERSION file.110+ // A commented-out literal is not read, versionNameSuffix is not a111+ // versionName, and a call is not a literal however many strings it passes:112+ // with nothing left, the refusal names a VERSION file.112113 write(root, 'build.gradle', '// versionName "9.9.9"\nversionNameSuffix = "-beta"\n')!113114 if _ := project_version(root) {114115 assert false, 'nothing was readable, so the version must be refused'115116 } else {116117 assert err.msg().contains('VERSION')117118 }119+120+ // A quoted argument inside a call is not the version.121+ write(root, 'build.gradle', "versionName = providers.gradleProperty('VERSION').get()\n")122+ if _ := project_version(root) {123+ assert false, 'a versionName built from a function call must not be read'124+ } else {125+ assert err.msg().contains('VERSION')126+ }127+ // `version.workspace = true` is Cargo delegating to the workspace, not a128+ // version either.129+ write(root, 'Cargo.toml', '[package]\nversion.workspace = true\n')130+ if _ := project_version(root) {131+ assert false, 'a version delegated to the workspace must not be read'132+ } else {133+ assert err.msg().contains('VERSION')134+ }118135 }119136 120137 fn test_read_config_parses_the_file_and_ignores_what_it_does_not_use() {@@ -266,5 +283,24 @@266283 assert false, 'a build with no output must be refused'267284 } else {268285 assert err.msg().contains('did not create')286+ }287+}288+289+// A build that leaves an empty file did not build anything: 0 bytes would be290+// published as a release for that platform.291+fn test_build_one_refuses_an_empty_artifact() {292+ root := scratch('build_zero')!293+ defer {294+ os.rmdir_all(root) or {}295+ }296+ write(root, 'koh.build', 'command = mkdir -p out && : > out/demo297+output = out/demo298+platforms = macos-aarch64299+')!300+ config := read_config(root)!301+ if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {302+ assert false, 'an empty artifact must be refused'303+ } else {304+ assert err.msg().contains('produced an empty')269305 }270306 }src/build/version.v+19 −15
@@ -81,28 +81,32 @@8181 if !trimmed.starts_with(key) {8282 continue8383 }84- if value := quoted_value(trimmed, key.len) {84+ if value := literal_after(trimmed, key.len) {8585 return value8686 }8787 }8888 return none8989 }9090 91-// quoted_value is the first quoted literal at or after `from` on a line. Only a92-// literal counts: `versionName = project.version` yields nothing.93-fn quoted_value(line string, from int) ?string {94- for at in from .. line.len {95- if line[at] != `"` && line[at] != `'` {96- continue97- }98- quote := line[at]99- for end in at + 1 .. line.len {100- if line[end] == quote {101- return line[at + 1..end]102- }103- }91+// literal_after is the quoted literal that starts right after the key, an92+// optional `=` and any spaces. That is what "assigned a quoted string literal"93+// means: a call such as `gradleProperty("VERSION")` or a name such as94+// `project.version` starts with something else, so it yields nothing and the95+// next source is tried.96+fn literal_after(line string, from int) ?string {97+ mut at := from98+ for at < line.len && line[at] in [` `, `\t`, `=`, `:`] {99+ at++100+ }101+ if at >= line.len || (line[at] != `"` && line[at] != `'`) {104102 return none105103 }104+ quote := line[at]105+ for end in at + 1 .. line.len {106+ if line[end] == quote {107+ return line[at + 1..end]108+ }109+ }106110 return none107111 }108112 @@ -141,7 +145,7 @@141145 || (trimmed['versionName'.len] != ` ` && trimmed['versionName'.len] != `=`) {142146 continue143147 }144- if value := quoted_value(trimmed, 'versionName'.len) {148+ if value := literal_after(trimmed, 'versionName'.len) {145149 return value146150 }147151 }tests/cli_test.v+23 −0
@@ -146,6 +146,29 @@146146 assert !picked.out.contains('out/demo-macos-aarch64')147147 }148148 149+// The item's exit check, as a test: `koh build` builds Koh 3 itself with the150+// real toolchain, in a copy, and the binary it produces runs.151+fn test_build_builds_koh_itself() {152+ dir := temp_repo('selfbuild')153+ defer { os.rmdir_all(dir) or {} }154+ // Only what a build reads: the copy needs no history and no archive.155+ for item in ['src', 'vendor', 'docs', 'v.mod', 'VERSION'] {156+ os.cp_all(os.join_path(root, item), os.join_path(dir, item), true) or { panic(err) }157+ }158+ write(dir, 'koh.build', 'command = mkdir -p zig-out/bin && v -o zig-out/bin/koh3 src/koh3.v159+output = zig-out/bin/koh3160+platforms = ${this_platform()}161+')162+ init_repo(dir)163+ built := run(dir, 'build')164+ assert built.code == 0, built.err165+ assert built.out.contains('zig-out/bin/koh3')166+ fresh := os.join_path(dir, 'zig-out/bin/koh3')167+ assert os.exists(fresh)168+ version := os.read_file(os.join_path(dir, 'VERSION'))!.trim_space()169+ assert os.execute('${q(fresh)} --version').output.trim_space() == 'koh ${version}'170+}171+149172 fn test_build_refuses_what_it_cannot_do() {150173 dir := temp_repo('build_bad')151174 defer { os.rmdir_all(dir) or {} }v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.79'3+ version: '3.0.80'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }