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\nlang = cobol\n', 'cobol'],196 ]197 for case in cases {198 write(root, 'koh.build', case[0])!199 if _ := read_config(root) {200 assert false, case[1]201 } else {202 assert err.msg().contains(case[1]), err.msg()203 }204 }205}206207fn test_the_build_environment_is_koh_2s() {208 zig := env_for(.zig, 'linux-aarch64')209 assert zig['KOH_TARGET'] == 'aarch64-linux-musl'210 assert zig['KEPR_TARGET'] == 'aarch64-linux-musl'211 212 go_env := env_for(.go, 'linux-x86_64')213 assert go_env['KOH_TARGET'] == 'linux-x86_64'214 assert go_env['KEPR_TARGET'] == 'linux-x86_64'215 assert go_env['GOOS'] == 'linux'216 assert go_env['GOARCH'] == 'amd64'217 assert env_for(.rust, 'windows-x86_64')['CARGO_BUILD_TARGET'] == 'x86_64-pc-windows-gnu'218 assert env_for(.rust, 'macos-aarch64')['CARGO_BUILD_TARGET'] == 'aarch64-apple-darwin'219 assert env_for(.c, 'linux-x86_64')['CC'] == 'x86_64-linux-musl-gcc'220 221 assert 'CC' !in env_for(.c, 'macos-x86_64')222 assert env_for(.gradle, 'android-arm64').len == 2223 assert env_for(.node, 'android-x86')['KEPR_TARGET'] == 'android-x86'224}225226fn test_this_machine_is_one_of_the_platforms() {227 assert this_machine()! in platforms228}229230231232233234fn test_build_one_runs_the_command_and_finds_the_output() {235 root := scratch('build_one')!236 defer {237 os.rmdir_all(root) or {}238 }239 write(root, 'koh.build', 'command = mkdir -p out && env > out/demo-1.2.3240output = out/demo-{version}241platforms = macos-aarch64242lang = make243')!244 config := read_config(root)!245 built := build_one(root, config, 'macos-aarch64', '1.2.3')!246 assert built.platform == 'macos-aarch64'247 assert built.path == 'out/demo-1.2.3'248 assert built.size > 0249 seen := os.read_file(os.join_path(root, 'out/demo-1.2.3'))!250 assert seen.contains('KEPR_TARGET=macos-aarch64')251 assert seen.contains('KOH_TARGET=macos-aarch64')252}253254fn test_build_one_refuses_a_failing_build_and_shows_what_it_said() {255 root := scratch('build_fail')!256 defer {257 os.rmdir_all(root) or {}258 }259 write(root, 'koh.build', 'command = sh -c \'echo "no compiler here" >&2; exit 1\'260output = out/demo261platforms = macos-aarch64262')263 config := read_config(root)!264 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {265 assert false, 'a failing build must be refused'266 } else {267 assert err.msg().contains('no compiler here')268 }269}270271272fn test_build_one_refuses_a_build_that_creates_nothing() {273 root := scratch('build_empty')!274 defer {275 os.rmdir_all(root) or {}276 }277 write(root, 'koh.build', 'command = true278output = out/demo279platforms = macos-aarch64280')281 config := read_config(root)!282 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {283 assert false, 'a build with no output must be refused'284 } else {285 assert err.msg().contains('did not create')286 }287}288289290291fn 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/demo297output = out/demo298platforms = 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')305 }306}307308309310fn test_build_one_takes_background_children_with_it() {311 root := scratch('build_orphan')!312 defer {313 os.rmdir_all(root) or {}314 }315 write(root, 'koh.build', "command = mkdir -p out && printf early > out/demo && { sh -c 'sleep 2; printf late > out/demo' & }316output = out/demo317platforms = macos-aarch64318")!319 config := read_config(root)!320 built := build_one(root, config, 'macos-aarch64', '1.2.3')!321 assert built.size == 5322 323 time.sleep(3 * time.second)324 assert os.read_file(os.join_path(root, 'out/demo'))! == 'early'325}