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 write(root, 'build.gradle', '// versionName "9.9.9"\nversionNameSuffix = "-beta"\n')!113 if _ := project_version(root) {114 assert false, 'nothing was readable, so the version must be refused'115 } else {116 assert err.msg().contains('VERSION')117 }118}119120fn test_read_config_parses_the_file_and_ignores_what_it_does_not_use() {121 root := scratch('config')!122 defer {123 os.rmdir_all(root) or {}124 }125 write(root, 'koh.build', '# how demo is built126enabled = true127trigger = on-offer128timeout_seconds = 900129command = ./make.sh130output = out/demo-{version}-{platform}131platforms = macos-aarch64, linux-aarch64132lang = Zig133')134 config := read_config(root)!135 assert config.command == './make.sh'136 assert config.output == 'out/demo-{version}-{platform}'137 assert config.platforms == ['macos-aarch64', 'linux-aarch64']138 assert config.lang == .zig139 assert output_path(config, 'linux-aarch64', '1.2.3') == 'out/demo-1.2.3-linux-aarch64'140}141142143144fn test_the_language_is_detected_from_the_project() {145 root := scratch('detect')!146 defer {147 os.rmdir_all(root) or {}148 }149 write(root, 'koh.build', 'command = true\noutput = a\nplatforms = macos-aarch64\n')!150 write(root, 'build.zig', '// a zig project\n')!151 assert read_config(root)!.lang == .zig152 write(root, 'build.sh', '#!/bin/sh\n')!153 assert read_config(root)!.lang == .shell154}155156fn test_read_config_refuses_what_it_cannot_build() {157 root := scratch('config_bad')!158 defer {159 os.rmdir_all(root) or {}160 }161 if _ := read_config(root) {162 assert false, 'a missing koh.build must be refused'163 } else {164 assert err.msg().contains('koh.build')165 }166}167168fn test_read_config_refuses_each_missing_key() {169 root := scratch('config_keys')!170 defer {171 os.rmdir_all(root) or {}172 }173 cases := [174 ['command = true\nplatforms = macos-aarch64\n', 'has no output'],175 ['output = a\nplatforms = macos-aarch64\n', 'has no command'],176 ['command = true\noutput = a\n', 'names no platforms'],177 ['command = true\noutput = a\nplatforms = macos-aarch64, amiga\n', 'amiga'],178 ['command = true\noutput = a\nplatforms = macos-aarch64\nlang = cobol\n', 'cobol'],179 ]180 for case in cases {181 write(root, 'koh.build', case[0])!182 if _ := read_config(root) {183 assert false, case[1]184 } else {185 assert err.msg().contains(case[1]), err.msg()186 }187 }188}189190fn test_the_build_environment_is_koh_2s() {191 zig := env_for(.zig, 'linux-aarch64')192 assert zig['KOH_TARGET'] == 'aarch64-linux-musl'193 assert zig['KEPR_TARGET'] == 'aarch64-linux-musl'194 195 go_env := env_for(.go, 'linux-x86_64')196 assert go_env['KOH_TARGET'] == 'linux-x86_64'197 assert go_env['KEPR_TARGET'] == 'linux-x86_64'198 assert go_env['GOOS'] == 'linux'199 assert go_env['GOARCH'] == 'amd64'200 assert env_for(.rust, 'windows-x86_64')['CARGO_BUILD_TARGET'] == 'x86_64-pc-windows-gnu'201 assert env_for(.rust, 'macos-aarch64')['CARGO_BUILD_TARGET'] == 'aarch64-apple-darwin'202 assert env_for(.c, 'linux-x86_64')['CC'] == 'x86_64-linux-musl-gcc'203 204 assert 'CC' !in env_for(.c, 'macos-x86_64')205 assert env_for(.gradle, 'android-arm64').len == 2206 assert env_for(.node, 'android-x86')['KEPR_TARGET'] == 'android-x86'207}208209fn test_this_machine_is_one_of_the_platforms() {210 assert this_machine()! in platforms211}212213214215216217fn test_build_one_runs_the_command_and_finds_the_output() {218 root := scratch('build_one')!219 defer {220 os.rmdir_all(root) or {}221 }222 write(root, 'koh.build', 'command = mkdir -p out && env > out/demo-1.2.3223output = out/demo-{version}224platforms = macos-aarch64225lang = make226')!227 config := read_config(root)!228 built := build_one(root, config, 'macos-aarch64', '1.2.3')!229 assert built.platform == 'macos-aarch64'230 assert built.path == 'out/demo-1.2.3'231 assert built.size > 0232 seen := os.read_file(os.join_path(root, 'out/demo-1.2.3'))!233 assert seen.contains('KEPR_TARGET=macos-aarch64')234 assert seen.contains('KOH_TARGET=macos-aarch64')235}236237fn test_build_one_refuses_a_failing_build_and_shows_what_it_said() {238 root := scratch('build_fail')!239 defer {240 os.rmdir_all(root) or {}241 }242 write(root, 'koh.build', 'command = sh -c \'echo "no compiler here" >&2; exit 1\'243output = out/demo244platforms = macos-aarch64245')246 config := read_config(root)!247 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {248 assert false, 'a failing build must be refused'249 } else {250 assert err.msg().contains('no compiler here')251 }252}253254255fn test_build_one_refuses_a_build_that_creates_nothing() {256 root := scratch('build_empty')!257 defer {258 os.rmdir_all(root) or {}259 }260 write(root, 'koh.build', 'command = true261output = out/demo262platforms = macos-aarch64263')264 config := read_config(root)!265 if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {266 assert false, 'a build with no output must be refused'267 } else {268 assert err.msg().contains('did not create')269 }270}