12345678module build910#include <signal.h>1112import os13import time1415pub const config_name = 'koh.build'16171819pub const platforms = ['linux-x86_64', 'linux-aarch64', 'macos-aarch64', 'macos-x86_64',20 'windows-x86_64', 'android-arm64', 'android-x86']21222324const build_timeout = 10 * time.minute2526const lang_names = 'zig, rust, go, c, node, swift, python, shell, make, gradle'272829pub enum Lang {30 unknown31 zig32 rust33 go34 c35 node36 swift37 python38 shell39 make40 gradle41}4243pub struct Config {44pub:45 command string46 output string47 platforms []string48 lang Lang49}505152struct Probe {53 file string54 lang Lang55}565758596061pub fn read_config(root string) !Config {62 text := os.read_file(os.join_path(root, config_name)) or {63 return error('there is no ${config_name} in ${os.base(root)} — see "koh man build"')64 }65 mut command := ''66 mut output := ''67 mut named := ''68 mut lang := Lang.unknown69 for line in text.split_into_lines() {70 trimmed := line.trim_space()71 if trimmed == '' || trimmed.starts_with('#') {72 continue73 }74 at := trimmed.index('=') or { continue }75 key := trimmed[..at].trim_space()76 value := trimmed[at + 1..].trim_space()77 match key {78 'command' { command = value }79 'output' { output = value }80 'platforms' { named = value }81 'lang' {82 lang = lang_of(value) or {83 return error('${config_name} says lang = ${value}, which is not one of ${lang_names}')84 }85 }86 else {}87 }88 }89 if command == '' {90 return error('${config_name} has no command')91 }92 if output == '' {93 return error('${config_name} has no output')94 }95 platform_list := named.split(',').map(it.trim_space()).filter(it != '')96 if platform_list.len == 0 {97 return error('${config_name} names no platforms')98 }99 for platform in platform_list {100 if platform !in platforms {101 return error('${config_name} names platform "${platform}", which is not one of ${platforms.join(', ')}')102 }103 }104 return Config{105 command: command106 output: output107 platforms: platform_list108 lang: if lang == .unknown { detect(root) } else { lang }109 }110}111112113114fn lang_of(name string) ?Lang {115 return match name.to_lower() {116 'zig' { Lang.zig }117 'rust' { Lang.rust }118 'go' { Lang.go }119 'c' { Lang.c }120 'node' { Lang.node }121 'swift' { Lang.swift }122 'python' { Lang.python }123 'shell' { Lang.shell }124 'make' { Lang.make }125 'gradle' { Lang.gradle }126 else { none }127 }128}129130131132fn detect(root string) Lang {133 for probe in [134 Probe{'build.sh', Lang.shell},135 Probe{'build.zig', Lang.zig},136 Probe{'Cargo.toml', Lang.rust},137 Probe{'go.mod', Lang.go},138 Probe{'Package.swift', Lang.swift},139 Probe{'pyproject.toml', Lang.python},140 Probe{'setup.py', Lang.python},141 Probe{'package.json', Lang.node},142 Probe{'app/build.gradle.kts', Lang.gradle},143 Probe{'app/build.gradle', Lang.gradle},144 Probe{'build.gradle.kts', Lang.gradle},145 Probe{'build.gradle', Lang.gradle},146 Probe{'CMakeLists.txt', Lang.c},147 Probe{'meson.build', Lang.c},148 Probe{'configure', Lang.c},149 Probe{'Makefile', Lang.make},150 Probe{'makefile', Lang.make},151 ] {152 if os.exists(os.join_path(root, probe.file)) {153 return probe.lang154 }155 }156 return .unknown157}158159160161pub fn this_machine() !string {162 machine := os.uname().machine163 arch := if machine in ['arm64', 'aarch64'] {164 'aarch64'165 } else if machine in ['x86_64', 'amd64'] {166 'x86_64'167 } else {168 return error('this machine is "${machine}" — koh builds for ${platforms.join(', ')}')169 }170 name := '${os.user_os()}-${arch}'171 if name !in platforms {172 return error('this machine is ${name} — koh builds for ${platforms.join(', ')}')173 }174 return name175}176177178179180struct Cross {181 goos string182 goarch string183 rust string184 cc string185}186187fn cross_of(platform string) Cross {188 return match platform {189 'linux-x86_64' {190 Cross{'linux', 'amd64', 'x86_64-unknown-linux-musl', 'x86_64-linux-musl-gcc'}191 }192 'linux-aarch64' {193 Cross{'linux', 'arm64', 'aarch64-unknown-linux-musl', 'aarch64-linux-musl-gcc'}194 }195 'macos-aarch64' { Cross{'darwin', 'arm64', 'aarch64-apple-darwin', ''} }196 'macos-x86_64' { Cross{'darwin', 'amd64', 'x86_64-apple-darwin', ''} }197 'windows-x86_64' { Cross{'windows', 'amd64', 'x86_64-pc-windows-gnu', ''} }198 else { Cross{} }199 }200}201202203204pub fn env_for(lang Lang, platform string) map[string]string {205 if lang == .zig {206 207 208 triple := zig_triple(platform)209 return {210 'KOH_TARGET': triple211 'KEPR_TARGET': triple212 }213 }214 mut env := {215 'KOH_TARGET': platform216 'KEPR_TARGET': platform217 }218 cross := cross_of(platform)219 if cross.goos == '' {220 return env221 }222 if lang == .go {223 env['GOOS'] = cross.goos224 env['GOARCH'] = cross.goarch225 } else if lang == .rust {226 env['CARGO_BUILD_TARGET'] = cross.rust227 } else if lang == .c && cross.cc != '' {228 env['CC'] = cross.cc229 }230 return env231}232233234235fn zig_triple(platform string) string {236 return match platform {237 'linux-x86_64' { 'x86_64-linux-musl' }238 'linux-aarch64' { 'aarch64-linux-musl' }239 'macos-aarch64' { 'aarch64-macos' }240 'macos-x86_64' { 'x86_64-macos' }241 'windows-x86_64' { 'x86_64-windows' }242 else { platform }243 }244}245246247248pub fn output_path(config Config, platform string, version string) string {249 return config.output.replace('{version}', version).replace('{platform}', platform)250}251252pub struct Built {253pub:254 platform string255 path string256 size i64257}258259260261pub fn build_one(root string, config Config, platform string, version string) !Built {262 path := output_path(config, platform, version)263 run := run_build(root, config.command, env_for(config.lang, platform))!264 if run.timed_out {265 return error('the ${platform} build was still running after ten minutes and was stopped')266 }267 if run.code != 0 {268 return error('the ${platform} build failed:\n${tail(run.output)}')269 }270 full := os.join_path(root, path)271 if !os.exists(full) {272 return error('the ${platform} build did not create ${path}')273 }274 if os.is_dir(full) {275 return error('${path} is a folder — a release file is one file')276 }277 size := os.file_size(full)278 if size == 0 {279 return error('the ${platform} build produced an empty ${path}')280 }281 return Built{282 platform: platform283 path: path284 size: size285 }286}287288289290struct Run {291 code int292 timed_out bool293 output string294}295296297298299fn run_build(root string, command string, env map[string]string) !Run {300 logs := os.join_path(os.temp_dir(), 'koh-build-${os.getpid()}-${time.now().unix_nano()}')301 os.mkdir_all(logs, os.MkdirParams{})!302 defer {303 os.rmdir_all(logs) or {}304 }305 stdout_path := os.join_path(logs, 'stdout')306 stderr_path := os.join_path(logs, 'stderr')307 mut environment := os.environ()308 for key, value in env {309 environment[key] = value310 }311 mut child := os.new_process('sh')312 313 314 315 child.set_args(['-c', '{ ${command} ; } > ${sh_quote(stdout_path)} 2> ${sh_quote(stderr_path)}'])316 child.set_work_folder(root)317 child.set_environment(environment)318 319 child.use_pgroup = true320 child.run()321 deadline := time.now().add(build_timeout)322 for child.is_alive() && time.now() < deadline {323 time.sleep(200 * time.millisecond)324 }325 timed_out := child.is_alive()326 327 328 329 330 331 332 if child.pid > 0 {333 C.kill(-child.pid, C.SIGKILL)334 }335 if timed_out {336 337 child.wait()338 }339 stdout := os.read_file(stdout_path) or { '' }340 stderr := os.read_file(stderr_path) or { '' }341 return Run{342 code: child.code343 timed_out: timed_out344 output: '${stdout}${stderr}'345 }346}347348349350fn sh_quote(text string) string {351 return "'" + text.replace("'", "'\\''") + "'"352}353354355356fn tail(text string) string {357 lines := text.trim_right('\n').split_into_lines()358 if lines.len <= 20 {359 return lines.join('\n')360 }361 return lines[lines.len - 20..].join('\n')362}