12345678module build910import os11import time1213pub const config_name = 'koh.build'14151617pub const platforms = ['linux-x86_64', 'linux-aarch64', 'macos-aarch64', 'macos-x86_64',18 'windows-x86_64', 'android-arm64', 'android-x86']19202122const build_timeout = 10 * time.minute2324const lang_names = 'zig, rust, go, c, node, swift, python, shell, make, gradle'252627pub enum Lang {28 unknown29 zig30 rust31 go32 c33 node34 swift35 python36 shell37 make38 gradle39}4041pub struct Config {42pub:43 command string44 output string45 platforms []string46 lang Lang47}484950struct Probe {51 file string52 lang Lang53}545556575859pub fn read_config(root string) !Config {60 text := os.read_file(os.join_path(root, config_name)) or {61 return error('there is no ${config_name} in ${os.base(root)} — see "koh man build"')62 }63 mut command := ''64 mut output := ''65 mut named := ''66 mut lang := Lang.unknown67 for line in text.split_into_lines() {68 trimmed := line.trim_space()69 if trimmed == '' || trimmed.starts_with('#') {70 continue71 }72 at := trimmed.index('=') or { continue }73 key := trimmed[..at].trim_space()74 value := trimmed[at + 1..].trim_space()75 match key {76 'command' { command = value }77 'output' { output = value }78 'platforms' { named = value }79 'lang' {80 lang = lang_of(value) or {81 return error('${config_name} says lang = ${value}, which is not one of ${lang_names}')82 }83 }84 else {}85 }86 }87 if command == '' {88 return error('${config_name} has no command')89 }90 if output == '' {91 return error('${config_name} has no output')92 }93 platform_list := named.split(',').map(it.trim_space()).filter(it != '')94 if platform_list.len == 0 {95 return error('${config_name} names no platforms')96 }97 for platform in platform_list {98 if platform !in platforms {99 return error('${config_name} names platform "${platform}", which is not one of ${platforms.join(', ')}')100 }101 }102 return Config{103 command: command104 output: output105 platforms: platform_list106 lang: if lang == .unknown { detect(root) } else { lang }107 }108}109110111112fn lang_of(name string) ?Lang {113 return match name.to_lower() {114 'zig' { Lang.zig }115 'rust' { Lang.rust }116 'go' { Lang.go }117 'c' { Lang.c }118 'node' { Lang.node }119 'swift' { Lang.swift }120 'python' { Lang.python }121 'shell' { Lang.shell }122 'make' { Lang.make }123 'gradle' { Lang.gradle }124 else { none }125 }126}127128129130fn detect(root string) Lang {131 for probe in [132 Probe{'build.sh', Lang.shell},133 Probe{'build.zig', Lang.zig},134 Probe{'Cargo.toml', Lang.rust},135 Probe{'go.mod', Lang.go},136 Probe{'Package.swift', Lang.swift},137 Probe{'pyproject.toml', Lang.python},138 Probe{'setup.py', Lang.python},139 Probe{'package.json', Lang.node},140 Probe{'app/build.gradle.kts', Lang.gradle},141 Probe{'app/build.gradle', Lang.gradle},142 Probe{'build.gradle.kts', Lang.gradle},143 Probe{'build.gradle', Lang.gradle},144 Probe{'CMakeLists.txt', Lang.c},145 Probe{'meson.build', Lang.c},146 Probe{'configure', Lang.c},147 Probe{'Makefile', Lang.make},148 Probe{'makefile', Lang.make},149 ] {150 if os.exists(os.join_path(root, probe.file)) {151 return probe.lang152 }153 }154 return .unknown155}156157158159pub fn this_machine() !string {160 machine := os.uname().machine161 arch := if machine in ['arm64', 'aarch64'] {162 'aarch64'163 } else if machine in ['x86_64', 'amd64'] {164 'x86_64'165 } else {166 return error('this machine is "${machine}" — koh builds for ${platforms.join(', ')}')167 }168 name := '${os.user_os()}-${arch}'169 if name !in platforms {170 return error('this machine is ${name} — koh builds for ${platforms.join(', ')}')171 }172 return name173}174175176177178struct Cross {179 goos string180 goarch string181 rust string182 cc string183}184185fn cross_of(platform string) Cross {186 return match platform {187 'linux-x86_64' {188 Cross{'linux', 'amd64', 'x86_64-unknown-linux-musl', 'x86_64-linux-musl-gcc'}189 }190 'linux-aarch64' {191 Cross{'linux', 'arm64', 'aarch64-unknown-linux-musl', 'aarch64-linux-musl-gcc'}192 }193 'macos-aarch64' { Cross{'darwin', 'arm64', 'aarch64-apple-darwin', ''} }194 'macos-x86_64' { Cross{'darwin', 'amd64', 'x86_64-apple-darwin', ''} }195 'windows-x86_64' { Cross{'windows', 'amd64', 'x86_64-pc-windows-gnu', ''} }196 else { Cross{} }197 }198}199200201202pub fn env_for(lang Lang, platform string) map[string]string {203 if lang == .zig {204 205 206 triple := zig_triple(platform)207 return {208 'KOH_TARGET': triple209 'KEPR_TARGET': triple210 }211 }212 mut env := {213 'KOH_TARGET': platform214 'KEPR_TARGET': platform215 }216 cross := cross_of(platform)217 if cross.goos == '' {218 return env219 }220 if lang == .go {221 env['GOOS'] = cross.goos222 env['GOARCH'] = cross.goarch223 } else if lang == .rust {224 env['CARGO_BUILD_TARGET'] = cross.rust225 } else if lang == .c && cross.cc != '' {226 env['CC'] = cross.cc227 }228 return env229}230231232233fn zig_triple(platform string) string {234 return match platform {235 'linux-x86_64' { 'x86_64-linux-musl' }236 'linux-aarch64' { 'aarch64-linux-musl' }237 'macos-aarch64' { 'aarch64-macos' }238 'macos-x86_64' { 'x86_64-macos' }239 'windows-x86_64' { 'x86_64-windows' }240 else { platform }241 }242}243244245246pub fn output_path(config Config, platform string, version string) string {247 return config.output.replace('{version}', version).replace('{platform}', platform)248}249250pub struct Built {251pub:252 platform string253 path string254 size i64255}256257258259pub fn build_one(root string, config Config, platform string, version string) !Built {260 path := output_path(config, platform, version)261 run := run_build(root, config.command, env_for(config.lang, platform))!262 if run.timed_out {263 return error('the ${platform} build was still running after ten minutes and was stopped')264 }265 if run.code != 0 {266 return error('the ${platform} build failed:\n${tail(run.output)}')267 }268 full := os.join_path(root, path)269 if !os.exists(full) {270 return error('the ${platform} build did not create ${path}')271 }272 if os.is_dir(full) {273 return error('${path} is a folder — a release file is one file')274 }275 return Built{276 platform: platform277 path: path278 size: os.file_size(full)279 }280}281282283284struct Run {285 code int286 timed_out bool287 output string288}289290291292293fn run_build(root string, command string, env map[string]string) !Run {294 logs := os.join_path(os.temp_dir(), 'koh-build-${os.getpid()}-${time.now().unix_nano()}')295 os.mkdir_all(logs, os.MkdirParams{})!296 defer {297 os.rmdir_all(logs) or {}298 }299 stdout_path := os.join_path(logs, 'stdout')300 stderr_path := os.join_path(logs, 'stderr')301 mut environment := os.environ()302 for key, value in env {303 environment[key] = value304 }305 mut child := os.new_process('sh')306 307 308 309 child.set_args(['-c', '{ ${command} ; } > ${sh_quote(stdout_path)} 2> ${sh_quote(stderr_path)}'])310 child.set_work_folder(root)311 child.set_environment(environment)312 313 child.use_pgroup = true314 child.run()315 deadline := time.now().add(build_timeout)316 for child.is_alive() && time.now() < deadline {317 time.sleep(200 * time.millisecond)318 }319 timed_out := child.is_alive()320 if timed_out {321 child.signal_pgkill()322 child.wait()323 }324 stdout := os.read_file(stdout_path) or { '' }325 stderr := os.read_file(stderr_path) or { '' }326 return Run{327 code: child.code328 timed_out: timed_out329 output: '${stdout}${stderr}'330 }331}332333334335fn sh_quote(text string) string {336 return "'" + text.replace("'", "'\\''") + "'"337}338339340341fn tail(text string) string {342 lines := text.trim_right('\n').split_into_lines()343 if lines.len <= 20 {344 return lines.join('\n')345 }346 return lines[lines.len - 20..].join('\n')347}