1234module build56import os7import time89fn scratch(name string) !string {10 dir := os.join_path(os.temp_dir(), 'koh3-build-${name}-${time.now().unix_nano()}')11 os.mkdir_all(dir, os.MkdirParams{})!12 return dir13}1415fn write(root string, rel string, text string) ! {16 full := os.join_path(root, rel)17 os.mkdir_all(os.dir(full), os.MkdirParams{})!18 os.write_file(full, text)!19}2021struct SourceCase {22 file string23 content string24 want string25}2627fn test_every_version_source_is_read() {28 zon := '.{\n .name = "demo",\n .minimum_zig_version = "0.16.0",\n .version = "2.0.1",\n}\n'29 plist := '<?xml version="1.0"?>\n<dict>\n<key>CFBundleShortVersionString</key>\n<string>6.0.5</string>\n</dict>\n'30 cases := [31 SourceCase{'VERSION', '1.0.0\n', '1.0.0'},32 SourceCase{'build.zig.zon', zon, '2.0.1'},33 SourceCase{'v.mod', "Module {\n\tname: 'demo'\n\tversion: '3.0.2'\n}", '3.0.2'},34 SourceCase{'package.json', '{\n "name": "demo",\n "version": "4.0.3"\n}', '4.0.3'},35 SourceCase{'Cargo.toml', '[package]\nname = "demo"\nversion = "5.0.4"\n', '5.0.4'},36 SourceCase{'Info.plist', plist, '6.0.5'},37 SourceCase{'build.gradle.kts', 'versionName = "7.0.6"\n', '7.0.6'},38 SourceCase{'build.gradle', 'versionName "8.0.7"\n', '8.0.7'},39 SourceCase{'build.gradle', "versionName '9.0.8'\n", '9.0.8'},40 SourceCase{'app/build.gradle.kts', 'versionName = "10.0.9"\n', '10.0.9'},41 SourceCase{'app/build.gradle', "versionName '10.0.9'\n", '10.0.9'},42 ]43 for case in cases {44 root := scratch('source')!45 defer {46 os.rmdir_all(root) or {}47 }48 write(root, case.file, case.content)!49 assert project_version(root)! == case.want, case.file50 }51}525354fn test_a_version_file_wins_over_every_source() {55 root := scratch('override')!56 defer {57 os.rmdir_all(root) or {}58 }59 write(root, 'build.zig.zon', '.{\n .version = "2.0.1",\n}\n')!60 write(root, 'v.mod', "Module {\n\tversion: '3.0.2'\n}")!61 write(root, 'VERSION', '9.9.9\n')!62 assert project_version(root)! == '9.9.9'63}646566fn test_a_project_with_no_version_is_refused() {67 root := scratch('no_version')!68 defer {69 os.rmdir_all(root) or {}70 }71 if _ := project_version(root) {72 assert false, 'a project with no version must be refused'73 } else {74 assert err.msg().contains('VERSION')75 }76}77787980fn test_a_value_that_is_not_a_version_is_refused() {81 root := scratch('bad_version')!82 defer {83 os.rmdir_all(root) or {}84 }85 write(root, 'VERSION', '1.0.0 build 7\n')!86 if _ := project_version(root) {87 assert false, 'a value that is not a version must be refused'88 } else {89 assert err.msg().contains('VERSION')90 }91 write(root, 'VERSION', 'abcdefghijklmnopqrstuvwxyz0123456789\n')!92 if _ := project_version(root) {93 assert false, 'a version longer than 32 characters must be refused'94 } else {95 assert err.msg().contains('VERSION')96 }97}9899100101fn test_a_variable_version_name_falls_through() {102 root := scratch('gradle_variable')!103 defer {104 os.rmdir_all(root) or {}105 }106 write(root, 'app/build.gradle.kts', 'versionName = project.ext.version\n')!107 write(root, 'build.gradle', 'versionName "2.17.43"\n')!108 assert project_version(root)! == '2.17.43'109110 111 112 113 write(root, 'build.gradle', '// versionName "9.9.9"\nversionNameSuffix = "-beta"\n')!114 if _ := project_version(root) {115 assert false, 'nothing was readable, so the version must be refused'116 } else {117 assert err.msg().contains('VERSION')118 }119120 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 128 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 }135}136137fn test_read_config_parses_the_file_and_ignores_what_it_does_not_use() {138 root := scratch('config')!139 defer {140 os.rmdir_all(root) or {}141 }142 write(root, 'koh.build', '# how demo is built143enabled = true144trigger = on-offer145timeout_seconds = 900146command = ./make.sh147output = out/demo-{version}-{platform}148platforms = macos-aarch64, linux-aarch64149lang = Zig150')151 config := read_config(root)!152 assert config.command == './make.sh'153 assert config.output == 'out/demo-{version}-{platform}'154 assert config.platforms == ['macos-aarch64', 'linux-aarch64']155 assert config.lang == .zig156 assert output_path(config, 'linux-aarch64', '1.2.3') == 'out/demo-1.2.3-linux-aarch64'157}158159160161fn test_the_language_is_detected_from_the_project() {162 root := scratch('detect')!163 defer {164 os.rmdir_all(root) or {}165 }166 write(root, 'koh.build', 'command = true\noutput = a\nplatforms = macos-aarch64\n')!167 write(root, 'build.zig', '// a zig project\n')!168 assert read_config(root)!.lang == .zig169 write(root, 'build.sh', '#!/bin/sh\n')!170 assert read_config(root)!.lang == .shell171}172173fn test_read_config_refuses_what_it_cannot_build() {174 root := scratch('config_bad')!175 defer {176 os.rmdir_all(root) or {}177 }178 if _ := read_config(root) {179 assert false, 'a missing koh.build must be refused'180 } else {181 assert err.msg().contains('koh.build')182 }183}184185fn test_read_config_refuses_each_missing_key() {186 root := scratch('config_keys')!187 defer {188 os.rmdir_all(root) or {}189 }190 cases := [191 ['command = true\nplatforms = macos-aarch64\n', 'has no output'],192 ['output = a\nplatforms = macos-aarch64\n', 'has no command'],193 ['command = true\noutput = a\n', 'names no platforms'],194 ['command = true\noutput = a\nplatforms = macos-aarch64, amiga\n', 'amiga'],195 ['command = true\noutput = a\nplatforms = macos-aarch64, macos-aarch64\n', 'twice'],196 ['command = true\noutput = a\nplatforms = macos-aarch64\nlang = cobol\n', 'cobol'],197 ]198 for case in cases {199 write(root, 'koh.build', case[0])!200 if _ := read_config(root) {201 assert false, case[1]202 } else {203 assert err.msg().contains(case[1]), err.msg()204 }205 }206}207208fn test_the_build_environment_is_koh_2s() {209 zig := env_for(.zig, 'linux-aarch64')210 assert zig['KOH_TARGET'] == 'aarch64-linux-musl'211 assert zig['KEPR_TARGET'] == 'aarch64-linux-musl'212 213 go_env := env_for(.go, 'linux-x86_64')214 assert go_env['KOH_TARGET'] == 'linux-x86_64'215 assert go_env['KEPR_TARGET'] == 'linux-x86_64'216 assert go_env['GOOS'] == 'linux'217 assert go_env['GOARCH'] == 'amd64'218 assert env_for(.rust, 'windows-x86_64')['CARGO_BUILD_TARGET'] == 'x86_64-pc-windows-gnu'219 assert env_for(.rust, 'macos-aarch64')['CARGO_BUILD_TARGET'] == 'aarch64-apple-darwin'220 assert env_for(.c, 'linux-x86_64')['CC'] == 'x86_64-linux-musl-gcc'221 222 assert 'CC' !in env_for(.c, 'macos-x86_64')223 assert env_for(.gradle, 'android-arm64').len == 2224 assert env_for(.node, 'android-x86')['KEPR_TARGET'] == 'android-x86'225}226227fn test_this_machine_is_one_of_the_platforms() {228 assert this_machine()! in platforms229}230231232233234235fn test_build_one_runs_the_command_and_finds_the_output() {236 root := scratch('build_one')!237 defer {238 os.rmdir_all(root) or {}239 }240 write(root, 'koh.build', 'command = mkdir -p out && env > out/demo-1.2.3241output = out/demo-{version}242platforms = macos-aarch64243lang = make244')!245 config := read_config(root)!246 built := build_one(root, config, 'macos-aarch64', '1.2.3')!247 assert built.platform == 'macos-aarch64'248 assert built.path == 'out/demo-1.2.3'249 assert built.size > 0250 seen := os.read_file(os.join_path(root, 'out/demo-1.2.3'))!251 assert seen.contains('KEPR_TARGET=macos-aarch64')252 assert seen.contains('KOH_TARGET=macos-aarch64')253}254255fn test_build_one_refuses_a_failing_build_and_shows_what_it_said() {256 root := scratch('build_fail')!257 defer {258 os.rmdir_all(root) or {}259 }260 write(root, 'koh.build', 'command = sh -c \'echo "no compiler here" >&2; exit 1\'261output = out/demo262platforms = macos-aarch64263')264 config := read_config(root)!265 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {266 assert false, 'a failing build must be refused'267 } else {268 assert err.msg().contains('no compiler here')269 }270}271272273fn test_build_one_refuses_a_build_that_creates_nothing() {274 root := scratch('build_empty')!275 defer {276 os.rmdir_all(root) or {}277 }278 write(root, 'koh.build', 'command = true279output = out/demo280platforms = macos-aarch64281')282 config := read_config(root)!283 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {284 assert false, 'a build with no output must be refused'285 } else {286 assert err.msg().contains('did not create')287 }288}289290291292fn test_build_one_refuses_an_empty_artifact() {293 root := scratch('build_zero')!294 defer {295 os.rmdir_all(root) or {}296 }297 write(root, 'koh.build', 'command = mkdir -p out && : > out/demo298output = out/demo299platforms = macos-aarch64300')!301 config := read_config(root)!302 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {303 assert false, 'an empty artifact must be refused'304 } else {305 assert err.msg().contains('produced an empty')306 }307}308309310311fn test_build_one_takes_background_children_with_it() {312 root := scratch('build_orphan')!313 defer {314 os.rmdir_all(root) or {}315 }316 write(root, 'koh.build', "command = mkdir -p out && printf early > out/demo && { sh -c 'sleep 2; printf late > out/demo' & }317output = out/demo318platforms = macos-aarch64319")!320 config := read_config(root)!321 built := build_one(root, config, 'macos-aarch64', '1.2.3')!322 assert built.size == 5323 324 time.sleep(3 * time.second)325 assert os.read_file(os.join_path(root, 'out/demo'))! == 'early'326}