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 = 'v, zig, rust, go, c, cpp, c++, node, javascript, typescript, java, kotlin, scala, csharp, c#, cs, swift, python, ruby, php, dart, elixir, lua, r, shell, make, gradle'27282930pub enum Lang {31 unknown32 v33 zig34 rust35 go36 c37 cpp38 node39 java40 kotlin41 scala42 csharp43 swift44 python45 ruby46 php47 dart48 elixir49 lua50 r51 shell52 make53 gradle54}5556pub struct Config {57pub:58 command string59 output string60 platforms []string61 lang Lang62}636465struct Probe {66 file string67 lang Lang68}697071727374pub fn read_config(root string) !Config {75 text := os.read_file(os.join_path(root, config_name)) or {76 return error('there is no ${config_name} in ${os.base(root)} — see "koh man build"')77 }78 mut command := ''79 mut output := ''80 mut named := ''81 mut lang := Lang.unknown82 for line in text.split_into_lines() {83 trimmed := line.trim_space()84 if trimmed == '' || trimmed.starts_with('#') {85 continue86 }87 at := trimmed.index('=') or { continue }88 key := trimmed[..at].trim_space()89 value := trimmed[at + 1..].trim_space()90 match key {91 'command' { command = value }92 'output' { output = value }93 'platforms' { named = value }94 'lang' {95 lang = lang_of(value) or {96 return error('${config_name} says lang = ${value}, which is not one of ${lang_names}')97 }98 }99 else {}100 }101 }102 if command == '' {103 return error('${config_name} has no command')104 }105 if output == '' {106 return error('${config_name} has no output')107 }108 platform_list := named.split(',').map(it.trim_space()).filter(it != '')109 if platform_list.len == 0 {110 return error('${config_name} names no platforms')111 }112 mut seen := map[string]bool{}113 for platform in platform_list {114 if platform !in platforms {115 return error('${config_name} names platform "${platform}", which is not one of ${platforms.join(', ')}')116 }117 if seen[platform] {118 return error('${config_name} names platform "${platform}" twice')119 }120 seen[platform] = true121 }122 return Config{123 command: command124 output: output125 platforms: platform_list126 lang: if lang == .unknown { detect(root) } else { lang }127 }128}129130131132fn lang_of(name string) ?Lang {133 return match name.to_lower() {134 'v' { Lang.v }135 'zig' { Lang.zig }136 'rust' { Lang.rust }137 'go' { Lang.go }138 'c' { Lang.c }139 'cpp', 'c++' { Lang.cpp }140 'node', 'javascript', 'typescript' { Lang.node }141 'java' { Lang.java }142 'kotlin' { Lang.kotlin }143 'scala' { Lang.scala }144 'csharp', 'c#', 'cs' { Lang.csharp }145 'swift' { Lang.swift }146 'python' { Lang.python }147 'ruby' { Lang.ruby }148 'php' { Lang.php }149 'dart' { Lang.dart }150 'elixir' { Lang.elixir }151 'lua' { Lang.lua }152 'r' { Lang.r }153 'shell' { Lang.shell }154 'make' { Lang.make }155 'gradle' { Lang.gradle }156 else { none }157 }158}159160161162fn detect(root string) Lang {163 for probe in [164 Probe{'build.sh', Lang.shell},165 Probe{'v.mod', Lang.v},166 Probe{'build.zig', Lang.zig},167 Probe{'Cargo.toml', Lang.rust},168 Probe{'go.mod', Lang.go},169 Probe{'Package.swift', Lang.swift},170 Probe{'pyproject.toml', Lang.python},171 Probe{'setup.py', Lang.python},172 Probe{'package.json', Lang.node},173 Probe{'pom.xml', Lang.java},174 Probe{'build.sbt', Lang.scala},175 Probe{'Gemfile', Lang.ruby},176 Probe{'composer.json', Lang.php},177 Probe{'pubspec.yaml', Lang.dart},178 Probe{'mix.exs', Lang.elixir},179 Probe{'DESCRIPTION', Lang.r},180 Probe{'app/build.gradle.kts', Lang.gradle},181 Probe{'app/build.gradle', Lang.gradle},182 Probe{'build.gradle.kts', Lang.gradle},183 Probe{'build.gradle', Lang.gradle},184 Probe{'CMakeLists.txt', Lang.c},185 Probe{'meson.build', Lang.c},186 Probe{'configure', Lang.c},187 Probe{'Makefile', Lang.make},188 Probe{'makefile', Lang.make},189 ] {190 if os.exists(os.join_path(root, probe.file)) {191 return probe.lang192 }193 }194 return .unknown195}196197198199pub fn this_machine() !string {200 machine := os.uname().machine201 arch := if machine in ['arm64', 'aarch64'] {202 'aarch64'203 } else if machine in ['x86_64', 'amd64'] {204 'x86_64'205 } else {206 return error('this machine is "${machine}" — koh builds for ${platforms.join(', ')}')207 }208 name := '${os.user_os()}-${arch}'209 if name !in platforms {210 return error('this machine is ${name} — koh builds for ${platforms.join(', ')}')211 }212 return name213}214215216217218struct Cross {219 goos string220 goarch string221 rust string222 cc string223}224225fn cross_of(platform string) Cross {226 return match platform {227 'linux-x86_64' {228 Cross{'linux', 'amd64', 'x86_64-unknown-linux-musl', 'x86_64-linux-musl-gcc'}229 }230 'linux-aarch64' {231 Cross{'linux', 'arm64', 'aarch64-unknown-linux-musl', 'aarch64-linux-musl-gcc'}232 }233 'macos-aarch64' { Cross{'darwin', 'arm64', 'aarch64-apple-darwin', ''} }234 'macos-x86_64' { Cross{'darwin', 'amd64', 'x86_64-apple-darwin', ''} }235 'windows-x86_64' { Cross{'windows', 'amd64', 'x86_64-pc-windows-gnu', ''} }236 else { Cross{} }237 }238}239240241242pub fn env_for(lang Lang, platform string) map[string]string {243 if lang == .zig {244 245 246 triple := zig_triple(platform)247 return {248 'KOH_TARGET': triple249 'KEPR_TARGET': triple250 }251 }252 mut env := {253 'KOH_TARGET': platform254 'KEPR_TARGET': platform255 }256 cross := cross_of(platform)257 if cross.goos == '' {258 return env259 }260 if lang == .go {261 env['GOOS'] = cross.goos262 env['GOARCH'] = cross.goarch263 } else if lang == .rust {264 env['CARGO_BUILD_TARGET'] = cross.rust265 } else if lang == .c && cross.cc != '' {266 env['CC'] = cross.cc267 }268 return env269}270271272273fn zig_triple(platform string) string {274 return match platform {275 'linux-x86_64' { 'x86_64-linux-musl' }276 'linux-aarch64' { 'aarch64-linux-musl' }277 'macos-aarch64' { 'aarch64-macos' }278 'macos-x86_64' { 'x86_64-macos' }279 'windows-x86_64' { 'x86_64-windows' }280 else { platform }281 }282}283284285286pub fn output_path(config Config, platform string, version string) string {287 return config.output.replace('{version}', version).replace('{platform}', platform)288}289290pub struct Built {291pub:292 platform string293 path string294 size i64295}296297298299pub fn build_one(root string, config Config, platform string, version string) !Built {300 path := output_path(config, platform, version)301 run := run_build(root, config.command, env_for(config.lang, platform))!302 if run.timed_out {303 return error('the ${platform} build was still running after ten minutes and was stopped')304 }305 if run.code != 0 {306 return error('the ${platform} build failed:\n${tail(run.output)}')307 }308 full := os.join_path(root, path)309 if !os.exists(full) {310 return error('the ${platform} build did not create ${path}')311 }312 if os.is_dir(full) {313 return error('${path} is a folder — a release file is one file')314 }315 size := os.file_size(full)316 if size == 0 {317 return error('the ${platform} build produced an empty ${path}')318 }319 return Built{320 platform: platform321 path: path322 size: size323 }324}325326327328struct Run {329 code int330 timed_out bool331 output string332}333334335336337fn run_build(root string, command string, env map[string]string) !Run {338 logs := os.join_path(os.temp_dir(), 'koh-build-${os.getpid()}-${time.now().unix_nano()}')339 os.mkdir_all(logs, os.MkdirParams{})!340 defer {341 os.rmdir_all(logs) or {}342 }343 stdout_path := os.join_path(logs, 'stdout')344 stderr_path := os.join_path(logs, 'stderr')345 mut environment := os.environ()346 for key, value in env {347 environment[key] = value348 }349 mut child := os.new_process('sh')350 351 352 353 child.set_args(['-c', '{ ${command} ; } > ${sh_quote(stdout_path)} 2> ${sh_quote(stderr_path)}'])354 child.set_work_folder(root)355 child.set_environment(environment)356 357 child.use_pgroup = true358 child.run()359 deadline := time.now().add(build_timeout)360 for child.is_alive() && time.now() < deadline {361 time.sleep(200 * time.millisecond)362 }363 timed_out := child.is_alive()364 365 366 367 368 369 370 if child.pid > 0 {371 C.kill(-child.pid, C.SIGKILL)372 }373 if timed_out {374 375 child.wait()376 }377 stdout := os.read_file(stdout_path) or { '' }378 stderr := os.read_file(stderr_path) or { '' }379 return Run{380 code: child.code381 timed_out: timed_out382 output: '${stdout}${stderr}'383 }384}385386387388fn sh_quote(text string) string {389 return "'" + text.replace("'", "'\\''") + "'"390}391392393394fn tail(text string) string {395 lines := text.trim_right('\n').split_into_lines()396 if lines.len <= 20 {397 return lines.join('\n')398 }399 return lines[lines.len - 20..].join('\n')400}