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_every_defined_build_language_and_alias_is_accepted() {174 profiles := map[string]Lang{175 'v': .v176 'zig': .zig177 'rust': .rust178 'go': .go179 'c': .c180 'cpp': .cpp181 'c++': .cpp182 'node': .node183 'javascript': .node184 'typescript': .node185 'java': .java186 'kotlin': .kotlin187 'scala': .scala188 'csharp': .csharp189 'c#': .csharp190 'cs': .csharp191 'swift': .swift192 'python': .python193 'ruby': .ruby194 'php': .php195 'dart': .dart196 'elixir': .elixir197 'lua': .lua198 'r': .r199 'shell': .shell200 'make': .make201 'gradle': .gradle202 }203 for name, want in profiles {204 assert lang_of(name)? == want, name205 }206}207208fn test_mainstream_project_markers_are_detected() {209 markers := map[string]Lang{210 'v.mod': .v211 'pom.xml': .java212 'build.sbt': .scala213 'Gemfile': .ruby214 'composer.json': .php215 'pubspec.yaml': .dart216 'mix.exs': .elixir217 'DESCRIPTION': .r218 }219 for file, want in markers {220 root := scratch('detect_marker')!221 write(root, file, '')!222 assert detect(root) == want, file223 os.rmdir_all(root) or {}224 }225}226227fn test_read_config_refuses_what_it_cannot_build() {228 root := scratch('config_bad')!229 defer {230 os.rmdir_all(root) or {}231 }232 if _ := read_config(root) {233 assert false, 'a missing koh.build must be refused'234 } else {235 assert err.msg().contains('koh.build')236 }237}238239fn test_read_config_refuses_each_missing_key() {240 root := scratch('config_keys')!241 defer {242 os.rmdir_all(root) or {}243 }244 cases := [245 ['command = true\nplatforms = macos-aarch64\n', 'has no output'],246 ['output = a\nplatforms = macos-aarch64\n', 'has no command'],247 ['command = true\noutput = a\n', 'names no platforms'],248 ['command = true\noutput = a\nplatforms = macos-aarch64, amiga\n', 'amiga'],249 ['command = true\noutput = a\nplatforms = macos-aarch64, macos-aarch64\n', 'twice'],250 ['command = true\noutput = a\nplatforms = macos-aarch64\nlang = cobol\n', 'cobol'],251 ]252 for case in cases {253 write(root, 'koh.build', case[0])!254 if _ := read_config(root) {255 assert false, case[1]256 } else {257 assert err.msg().contains(case[1]), err.msg()258 }259 }260}261262fn test_the_build_environment_is_koh_2s() {263 zig := env_for(.zig, 'linux-aarch64')264 assert zig['KOH_TARGET'] == 'aarch64-linux-musl'265 assert zig['KEPR_TARGET'] == 'aarch64-linux-musl'266 267 go_env := env_for(.go, 'linux-x86_64')268 assert go_env['KOH_TARGET'] == 'linux-x86_64'269 assert go_env['KEPR_TARGET'] == 'linux-x86_64'270 assert go_env['GOOS'] == 'linux'271 assert go_env['GOARCH'] == 'amd64'272 assert env_for(.rust, 'windows-x86_64')['CARGO_BUILD_TARGET'] == 'x86_64-pc-windows-gnu'273 assert env_for(.rust, 'macos-aarch64')['CARGO_BUILD_TARGET'] == 'aarch64-apple-darwin'274 assert env_for(.c, 'linux-x86_64')['CC'] == 'x86_64-linux-musl-gcc'275 276 assert 'CC' !in env_for(.c, 'macos-x86_64')277 assert env_for(.gradle, 'android-arm64').len == 2278 assert env_for(.node, 'android-x86')['KEPR_TARGET'] == 'android-x86'279}280281fn test_this_machine_is_one_of_the_platforms() {282 assert this_machine()! in platforms283}284285286287288289fn test_build_one_runs_the_command_and_finds_the_output() {290 root := scratch('build_one')!291 defer {292 os.rmdir_all(root) or {}293 }294 write(root, 'koh.build', 'command = mkdir -p out && env > out/demo-1.2.3295output = out/demo-{version}296platforms = macos-aarch64297lang = make298')!299 config := read_config(root)!300 built := build_one(root, config, 'macos-aarch64', '1.2.3')!301 assert built.platform == 'macos-aarch64'302 assert built.path == 'out/demo-1.2.3'303 assert built.size > 0304 seen := os.read_file(os.join_path(root, 'out/demo-1.2.3'))!305 assert seen.contains('KEPR_TARGET=macos-aarch64')306 assert seen.contains('KOH_TARGET=macos-aarch64')307}308309fn test_build_one_refuses_a_failing_build_and_shows_what_it_said() {310 root := scratch('build_fail')!311 defer {312 os.rmdir_all(root) or {}313 }314 write(root, 'koh.build', 'command = sh -c \'echo "no compiler here" >&2; exit 1\'315output = out/demo316platforms = macos-aarch64317')318 config := read_config(root)!319 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {320 assert false, 'a failing build must be refused'321 } else {322 assert err.msg().contains('no compiler here')323 }324}325326327fn test_build_one_refuses_a_build_that_creates_nothing() {328 root := scratch('build_empty')!329 defer {330 os.rmdir_all(root) or {}331 }332 write(root, 'koh.build', 'command = true333output = out/demo334platforms = macos-aarch64335')336 config := read_config(root)!337 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {338 assert false, 'a build with no output must be refused'339 } else {340 assert err.msg().contains('did not create')341 }342}343344345346fn test_build_one_refuses_an_empty_artifact() {347 root := scratch('build_zero')!348 defer {349 os.rmdir_all(root) or {}350 }351 write(root, 'koh.build', 'command = mkdir -p out && : > out/demo352output = out/demo353platforms = macos-aarch64354')!355 config := read_config(root)!356 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {357 assert false, 'an empty artifact must be refused'358 } else {359 assert err.msg().contains('produced an empty')360 }361}362363364365fn test_build_one_takes_background_children_with_it() {366 root := scratch('build_orphan')!367 defer {368 os.rmdir_all(root) or {}369 }370 write(root, 'koh.build', "command = mkdir -p out && printf early > out/demo && { sh -c 'sleep 2; printf late > out/demo' & }371output = out/demo372platforms = macos-aarch64373")!374 config := read_config(root)!375 built := build_one(root, config, 'macos-aarch64', '1.2.3')!376 assert built.size == 5377 378 time.sleep(3 * time.second)379 assert os.read_file(os.join_path(root, 'out/demo'))! == 'early'380}