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
6 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 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.791+3.0.80

view file

src/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{

view file

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 }

view file

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 	}

view file

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 {} }

view file

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 }

view file