jm

Odin for scripts: small packages and a runner, on core: only
Log | Files | Refs | README

main.odin (6102B)


      1 /*
      2 odin-run compiles a single-file Odin script once and runs the cached binary
      3 on every call after that, so a script behaves like an interpreted one.
      4 
      5 	#!/usr/bin/env odin-run
      6 	package main
      7 	...
      8 
      9 	odin-run script.odin [args...]     compile if stale, then run
     10 	odin-run -clean                    remove every cached binary
     11 
     12 The cache key covers the script's bytes and path, every .odin file in the
     13 collection (path, size, modification time), the odin binary itself, the
     14 target, and the build flags. Binaries live under the user cache directory in
     15 odin-run/<key>/.
     16 
     17 Environment:
     18 	ODIN                 odin binary; default: "odin" on PATH
     19 	ODIN_RUN_COLLECTION  root of the jm collection; default: baked in at install
     20 	ODIN_RUN_FLAGS       extra build flags, split on spaces, such as "-debug"
     21 	ODIN_RUN_VERBOSE=1   print the build command and cache path to stderr (exactly "1")
     22 */
     23 package main
     24 
     25 import "core:fmt"
     26 import "core:hash/xxhash"
     27 import "core:os"
     28 import "core:path/filepath"
     29 import "core:strings"
     30 
     31 import "jm:sh"
     32 
     33 // The collection root baked in by `just install`; ODIN_RUN_COLLECTION wins.
     34 JM_COLLECTION :: #config(JM_COLLECTION, "")
     35 
     36 EXE :: ".exe" when ODIN_OS == .Windows else ""
     37 
     38 main :: proc() {
     39 	args := os.args[1:]
     40 	if len(args) > 0 && args[0] == "--" {
     41 		args = args[1:]
     42 	}
     43 	if len(args) == 0 || args[0] == "-h" || args[0] == "--help" {
     44 		fmt.eprintln("usage: odin-run <script.odin> [args...] | odin-run -clean")
     45 		os.exit(2)
     46 	}
     47 	if args[0] == "-clean" {
     48 		root := cache_root()
     49 		if err := os.remove_all(root); err != nil && err != os.General_Error.Not_Exist {
     50 			fail("cannot remove %s: %v", root, err)
     51 		}
     52 		return
     53 	}
     54 
     55 	script := args[0]
     56 	script_args := args[1:]
     57 	verbose_flag, _ := os.lookup_env("ODIN_RUN_VERBOSE", context.temp_allocator)
     58 	verbose := verbose_flag == "1"
     59 
     60 	source, rerr := os.read_entire_file_from_path(script, context.allocator)
     61 	if rerr != nil {
     62 		fail("cannot read %s: %v", script, rerr)
     63 	}
     64 	abs_script, _ := filepath.abs(script)
     65 	collection := collection_root()
     66 	odin := odin_binary()
     67 	flags := build_flags()
     68 
     69 	key := cache_key(source, abs_script, collection, odin, flags)
     70 	dir, _ := filepath.join({cache_root(), key})
     71 	stem := filepath.stem(script)
     72 	bin, _ := filepath.join({dir, strings.concatenate({stem, EXE})})
     73 
     74 	if !os.is_file(bin) {
     75 		if err := os.make_directory_all(dir); err != nil && err != os.General_Error.Exist {
     76 			fail("cannot create %s: %v", dir, err)
     77 		}
     78 		argv := make([dynamic]string)
     79 		append(&argv, odin, "build", abs_script, "-file")
     80 		if collection != "" {
     81 			append(&argv, strings.concatenate({"-collection:jm=", collection}))
     82 		}
     83 		append(&argv, strings.concatenate({"-out:", bin}))
     84 		append(&argv, ..flags)
     85 		if verbose {
     86 			fmt.eprintln("odin-run: build:", strings.join(argv[:], " "))
     87 		}
     88 		code, ok := sh.exec_run(argv[:])
     89 		if !ok {
     90 			os.remove_all(dir)
     91 			os.exit(code if code > 0 else 1)
     92 		}
     93 		copy_runtime_libraries(dir, odin)
     94 	}
     95 	if verbose {
     96 		fmt.eprintln("odin-run: binary:", bin)
     97 	}
     98 
     99 	run := make([dynamic]string)
    100 	append(&run, bin)
    101 	append(&run, ..script_args)
    102 	code, _ := sh.exec_run(run[:])
    103 	os.exit(code)
    104 }
    105 
    106 cache_key :: proc(source: []byte, abs_script, collection, odin: string, flags: []string) -> string {
    107 	b := strings.builder_make()
    108 	strings.write_string(&b, abs_script)
    109 	strings.write_byte(&b, 0)
    110 	strings.write_string(&b, string(source))
    111 	strings.write_byte(&b, 0)
    112 	strings.write_string(&b, ODIN_OS_STRING)
    113 	strings.write_string(&b, ODIN_ARCH_STRING)
    114 	for f in flags {
    115 		strings.write_string(&b, f)
    116 		strings.write_byte(&b, 0)
    117 	}
    118 	write_stat(&b, odin)
    119 	if collection != "" {
    120 		w := os.walker_create(collection)
    121 		defer os.walker_destroy(&w)
    122 		for info in os.walker_walk(&w) {
    123 			if info.type == .Directory && (info.name == ".git" || info.name == "build") {
    124 				os.walker_skip_dir(&w)
    125 				continue
    126 			}
    127 			if info.type == .Regular && filepath.ext(info.name) == ".odin" {
    128 				strings.write_string(&b, info.fullpath)
    129 				fmt.sbprintf(&b, ":%d:%d\x00", info.size, info.modification_time._nsec)
    130 			}
    131 		}
    132 	}
    133 	digest := xxhash.XXH3_64_default(b.buf[:])
    134 	return fmt.aprintf("%016x", digest)
    135 }
    136 
    137 write_stat :: proc(b: ^strings.Builder, path: string) {
    138 	info, err := os.stat(path, context.temp_allocator)
    139 	if err != nil {
    140 		strings.write_string(b, path)
    141 		return
    142 	}
    143 	fmt.sbprintf(b, "%s:%d:%d\x00", info.fullpath, info.size, info.modification_time._nsec)
    144 }
    145 
    146 cache_root :: proc() -> string {
    147 	base, err := os.user_cache_dir(context.allocator)
    148 	if err != nil {
    149 		base, _ = os.temp_directory(context.allocator)
    150 	}
    151 	root, _ := filepath.join({base, "odin-run"})
    152 	return root
    153 }
    154 
    155 collection_root :: proc() -> string {
    156 	if v, found := os.lookup_env("ODIN_RUN_COLLECTION", context.allocator); found && v != "" {
    157 		return v
    158 	}
    159 	return JM_COLLECTION
    160 }
    161 
    162 odin_binary :: proc() -> string {
    163 	if v, found := os.lookup_env("ODIN", context.allocator); found && v != "" {
    164 		return v
    165 	}
    166 	if p, found := sh.which("odin"); found {
    167 		return p
    168 	}
    169 	fail("odin not found on PATH; set ODIN")
    170 }
    171 
    172 build_flags :: proc() -> []string {
    173 	flags := make([dynamic]string)
    174 	append(&flags, "-vet", "-strict-style")
    175 	extra, found := os.lookup_env("ODIN_RUN_FLAGS", context.allocator)
    176 	if found {
    177 		append(&flags, ..strings.fields(extra))
    178 	}
    179 	optimised := true
    180 	for f in flags {
    181 		if f == "-debug" || strings.has_prefix(f, "-o:") {
    182 			optimised = false
    183 		}
    184 	}
    185 	if optimised {
    186 		append(&flags, "-o:speed")
    187 	}
    188 	return flags[:]
    189 }
    190 
    191 // copy_runtime_libraries puts the DLLs a Windows binary needs beside it. The
    192 // Windows branch of the foreign import in vendor/curl/curl.odin links
    193 // lib/libcurl.lib, an import library, so libcurl.dll must be on the search path.
    194 copy_runtime_libraries :: proc(dir, odin: string) {
    195 	when ODIN_OS == .Windows {
    196 		root, found := os.lookup_env("ODIN_ROOT", context.allocator)
    197 		if !found || root == "" {
    198 			root = filepath.dir(odin)
    199 		}
    200 		dll, _ := filepath.join({root, "vendor", "curl", "lib", "libcurl.dll"})
    201 		if os.is_file(dll) {
    202 			dest, _ := filepath.join({dir, "libcurl.dll"})
    203 			os.copy_file(dest, dll)
    204 		}
    205 	}
    206 }
    207 
    208 fail :: proc(format: string, args: ..any) -> ! {
    209 	fmt.eprint("odin-run: ")
    210 	fmt.eprintfln(format, ..args)
    211 	os.exit(1)
    212 }