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 mut seen := map[string]bool{}100 for platform in platform_list {101 if platform !in platforms {102 return error('${config_name} names platform "${platform}", which is not one of ${platforms.join(', ')}')103 }104 if seen[platform] {105 return error('${config_name} names platform "${platform}" twice')106 }107 seen[platform] = true108 }109 return Config{110 command: command111 output: output112 platforms: platform_list113 lang: if lang == .unknown { detect(root) } else { lang }114 }115}116117118119fn lang_of(name string) ?Lang {120 return match name.to_lower() {121 'zig' { Lang.zig }122 'rust' { Lang.rust }123 'go' { Lang.go }124 'c' { Lang.c }125 'node' { Lang.node }126 'swift' { Lang.swift }127 'python' { Lang.python }128 'shell' { Lang.shell }129 'make' { Lang.make }130 'gradle' { Lang.gradle }131 else { none }132 }133}134135136137fn detect(root string) Lang {138 for probe in [139 Probe{'build.sh', Lang.shell},140 Probe{'build.zig', Lang.zig},141 Probe{'Cargo.toml', Lang.rust},142 Probe{'go.mod', Lang.go},143 Probe{'Package.swift', Lang.swift},144 Probe{'pyproject.toml', Lang.python},145 Probe{'setup.py', Lang.python},146 Probe{'package.json', Lang.node},147 Probe{'app/build.gradle.kts', Lang.gradle},148 Probe{'app/build.gradle', Lang.gradle},149 Probe{'build.gradle.kts', Lang.gradle},150 Probe{'build.gradle', Lang.gradle},151 Probe{'CMakeLists.txt', Lang.c},152 Probe{'meson.build', Lang.c},153 Probe{'configure', Lang.c},154 Probe{'Makefile', Lang.make},155 Probe{'makefile', Lang.make},156 ] {157 if os.exists(os.join_path(root, probe.file)) {158 return probe.lang159 }160 }161 return .unknown162}163164165166pub fn this_machine() !string {167 machine := os.uname().machine168 arch := if machine in ['arm64', 'aarch64'] {169 'aarch64'170 } else if machine in ['x86_64', 'amd64'] {171 'x86_64'172 } else {173 return error('this machine is "${machine}" — koh builds for ${platforms.join(', ')}')174 }175 name := '${os.user_os()}-${arch}'176 if name !in platforms {177 return error('this machine is ${name} — koh builds for ${platforms.join(', ')}')178 }179 return name180}181182183184185struct Cross {186 goos string187 goarch string188 rust string189 cc string190}191192fn cross_of(platform string) Cross {193 return match platform {194 'linux-x86_64' {195 Cross{'linux', 'amd64', 'x86_64-unknown-linux-musl', 'x86_64-linux-musl-gcc'}196 }197 'linux-aarch64' {198 Cross{'linux', 'arm64', 'aarch64-unknown-linux-musl', 'aarch64-linux-musl-gcc'}199 }200 'macos-aarch64' { Cross{'darwin', 'arm64', 'aarch64-apple-darwin', ''} }201 'macos-x86_64' { Cross{'darwin', 'amd64', 'x86_64-apple-darwin', ''} }202 'windows-x86_64' { Cross{'windows', 'amd64', 'x86_64-pc-windows-gnu', ''} }203 else { Cross{} }204 }205}206207208209pub fn env_for(lang Lang, platform string) map[string]string {210 if lang == .zig {211 212 213 triple := zig_triple(platform)214 return {215 'KOH_TARGET': triple216 'KEPR_TARGET': triple217 }218 }219 mut env := {220 'KOH_TARGET': platform221 'KEPR_TARGET': platform222 }223 cross := cross_of(platform)224 if cross.goos == '' {225 return env226 }227 if lang == .go {228 env['GOOS'] = cross.goos229 env['GOARCH'] = cross.goarch230 } else if lang == .rust {231 env['CARGO_BUILD_TARGET'] = cross.rust232 } else if lang == .c && cross.cc != '' {233 env['CC'] = cross.cc234 }235 return env236}237238239240fn zig_triple(platform string) string {241 return match platform {242 'linux-x86_64' { 'x86_64-linux-musl' }243 'linux-aarch64' { 'aarch64-linux-musl' }244 'macos-aarch64' { 'aarch64-macos' }245 'macos-x86_64' { 'x86_64-macos' }246 'windows-x86_64' { 'x86_64-windows' }247 else { platform }248 }249}250251252253pub fn output_path(config Config, platform string, version string) string {254 return config.output.replace('{version}', version).replace('{platform}', platform)255}256257pub struct Built {258pub:259 platform string260 path string261 size i64262}263264265266pub fn build_one(root string, config Config, platform string, version string) !Built {267 path := output_path(config, platform, version)268 run := run_build(root, config.command, env_for(config.lang, platform))!269 if run.timed_out {270 return error('the ${platform} build was still running after ten minutes and was stopped')271 }272 if run.code != 0 {273 return error('the ${platform} build failed:\n${tail(run.output)}')274 }275 full := os.join_path(root, path)276 if !os.exists(full) {277 return error('the ${platform} build did not create ${path}')278 }279 if os.is_dir(full) {280 return error('${path} is a folder — a release file is one file')281 }282 size := os.file_size(full)283 if size == 0 {284 return error('the ${platform} build produced an empty ${path}')285 }286 return Built{287 platform: platform288 path: path289 size: size290 }291}292293294295struct Run {296 code int297 timed_out bool298 output string299}300301302303304fn run_build(root string, command string, env map[string]string) !Run {305 logs := os.join_path(os.temp_dir(), 'koh-build-${os.getpid()}-${time.now().unix_nano()}')306 os.mkdir_all(logs, os.MkdirParams{})!307 defer {308 os.rmdir_all(logs) or {}309 }310 stdout_path := os.join_path(logs, 'stdout')311 stderr_path := os.join_path(logs, 'stderr')312 mut environment := os.environ()313 for key, value in env {314 environment[key] = value315 }316 mut child := os.new_process('sh')317 318 319 320 child.set_args(['-c', '{ ${command} ; } > ${sh_quote(stdout_path)} 2> ${sh_quote(stderr_path)}'])321 child.set_work_folder(root)322 child.set_environment(environment)323 324 child.use_pgroup = true325 child.run()326 deadline := time.now().add(build_timeout)327 for child.is_alive() && time.now() < deadline {328 time.sleep(200 * time.millisecond)329 }330 timed_out := child.is_alive()331 332 333 334 335 336 337 if child.pid > 0 {338 C.kill(-child.pid, C.SIGKILL)339 }340 if timed_out {341 342 child.wait()343 }344 stdout := os.read_file(stdout_path) or { '' }345 stderr := os.read_file(stderr_path) or { '' }346 return Run{347 code: child.code348 timed_out: timed_out349 output: '${stdout}${stderr}'350 }351}352353354355fn sh_quote(text string) string {356 return "'" + text.replace("'", "'\\''") + "'"357}358359360361fn tail(text string) string {362 lines := text.trim_right('\n').split_into_lines()363 if lines.len <= 20 {364 return lines.join('\n')365 }366 return lines[lines.len - 20..].join('\n')367}