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 }