123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 | // Shared state and helpers for the tomo CLI commands
#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "../ast.h"
#include "../config.h"
#include "../naming.h"
#include "../parse/files.h"
#include "../stdlib/list.h"
#include "../stdlib/path.h"
#include "../stdlib/print.h"
#include "common.h"
OptionalBool_t verbose = false, quiet = false, clean_build = false, source_mapping = true, install_target = false,
instrument = false, profiling = false, debugging = false;
bool zig_cache_dir_from_env = false, cross_compiling = false, link_macho = false;
uint32_t enabled_logs = 0;
Text_t target_root = Text(""), lib_root = Text(""), zig_libc_dir = Text(""), cc = Text(""), ar = Text(""),
optimization = Text("2"), link_optimizations = Text(""), config_summary = Text(""), ldlibs = Text("-lm"),
ldflags = Text("");
OptionalText_t cflags = Text("-Werror -fdollars-in-identifiers -std=gnu23 -Wno-trigraphs"
" -ffunction-sections -fdata-sections"
" -fno-signed-zeros"
" -fPIC -ggdb"
" -DGC_THREADS");
// optimization gets a concrete value from configure_codegen();
// opt_flag stays NONE until the user passes -O:
OptionalText_t opt_flag = NONE_TEXT, target = NONE_TEXT;
#ifdef __linux__
struct stat compiler_stat;
#endif
const char *log_prefix(logtype_t type) {
switch (type) {
case LOG_BUILD: return "\033[2m[build]\033[m ";
case LOG_SKIP: return "\033[2m[skip]\033[m ";
case LOG_COMMANDS: return "\033[2m[cmd]\033[m ";
default: return "";
}
}
void set_default_logs(uint32_t default_logs) {
enabled_logs = default_logs;
if (verbose) enabled_logs = ~0u;
else if (quiet) enabled_logs = 0;
}
void configure_codegen(Text_t opt_level, bool optimize) {
// A debug build has to be one a debugger can follow: optimized code
// reorders and folds away the statements the user wants to step through,
// and without `#line` directives a debugger has no .tm source to show at
// all. An explicit -O still wins, since sometimes a bug only shows up
// optimized, but the default becomes -O0 and source mapping is forced
// on either way.
if (debugging) {
if (opt_flag.tag == TEXT_NONE) opt_level = Text("0");
source_mapping = true;
// Turns `breakpoint()` from nothing into a call the debugger stops on
// (see stdlib/debugger.h). It is part of cflags, so it is also part of
// config_summary and of the precompiled header's fingerprint --
// toggling --debug rebuilds rather than reusing non-debug objects.
cflags = Texts(cflags, " -DTOMO_DEBUG_BUILD");
}
optimization = opt_level;
// Debug (-O0) builds get UBSan in trap mode: generated code is kept free
// of undefined behavior (lambdas take their userdata as `void *` to match
// the closure calling convention, function-to-closure promotion goes
// through typed shims, and the tagged small-int fast paths shift through
// uint64_t), so violations, including in user-written `C_code`, trap
// with an ILLEGAL INSTRUCTION crash report instead of misbehaving
// silently. Trap mode needs no UBSan runtime library, which the -nostdlib
// link couldn't provide anyway. Optimized builds skip the instrumentation
// for performance:
if (Text$equal_values(opt_level, Text("0")))
cflags = Texts(cflags, " -fsanitize=undefined -fsanitize-trap=undefined");
else cflags = Texts(cflags, " -fno-sanitize=undefined");
// Dead-code stripping (--gc-sections / -dead_strip) and debug-section
// compression (zstd) make smaller binaries but slow linking noticeably.
// Enable them only for optimized (build/install) artifacts; the fast
// run/eval path skips them since its binary is thrown away after one run.
if (optimize) {
// Debug builds skip the zstd debug-section compression: the vendored
// libbacktrace decompresses it natively for runtime stacktraces, but
// an external debugger only reads those sections if its own build has
// zstd support, so a debug build keeps its DWARF uncompressed.
link_optimizations = link_macho ? Text(" -Wl,-w,-dead_strip")
: debugging ? Text(" -Wl,--gc-sections")
: Text(" -Wl,--gc-sections -Wl,--compress-debug-sections=zstd");
} else {
link_optimizations = Text("");
}
update_config_summary();
}
void update_config_summary(void) {
config_summary = Texts("TOMO_VERSION=", TOMO_VERSION, "\n", "COMPILER=", cc, " ", cflags, " -O", optimization, "\n",
"SOURCE_MAPPING=", source_mapping ? Text("yes") : Text("no"), "\n",
"INSTRUMENTED=", instrument ? Text("yes") : Text("no"), "\n",
"DEBUGGING=", debugging ? Text("yes") : Text("no"), "\n");
}
const char *paths_str(List_t paths) {
Text_t result = EMPTY_TEXT;
for (int64_t i = 0; i < (int64_t)paths.length; i++) {
if (i > 0) result = Texts(result, Text(" "));
result = Texts(result, Path$as_text((Path_t *)(paths.data + i * paths.stride), false, &Path$info));
}
return Text$as_c_string(result);
}
// An XDG base directory (e.g. XDG_CACHE_HOME, falling back to ~/.cache),
// with "/tomo" appended:
Path_t xdg_tomo_dir(const char *env_var, const char *fallback) {
const char *base = getenv(env_var);
return (base && base[0] == '/') ? Path$from_str(String(base, "/tomo"))
: Path$expand_home(Path$from_str(String(fallback, "/tomo")));
}
// The OS component of a platform key like "x86_64-linux" (the part after the
// last dash; architectures never contain dashes):
const char *platform_os(const char *platform) {
const char *dash = strrchr(platform, '-');
return dash ? dash + 1 : platform;
}
// The `zig cc -target` triple for a platform key: Linux targets musl; every
// other OS's platform key is already a valid Zig target:
const char *platform_triple(const char *platform) {
if (streq(platform_os(platform), "linux")) return String(platform, "-musl");
return platform;
}
bool platform_supported(const char *platform) {
return strstr(" " TOMO_DIST_PLATFORMS " ", String(" ", platform, " ")) != NULL;
}
// Cross-compiling needs the *target* platform's libtomo, vendored libraries,
// and headers. They come from the target platform's Tomo distribution archive,
// whose lib/ + include/ trees get extracted into target_root. If they're not
// installed yet, offer to download them (or just do it if --install-target).
void ensure_target_installed(void) {
Path_t marker = Path$from_str(String(target_root, "/lib/tomo@", TOMO_VERSION, "/libtomo.a"));
if (Path$is_file(marker, true)) return;
const char *dist_url = getenv("TOMO_DIST_URL");
if (!dist_url || dist_url[0] == '\0') dist_url = "https://tomo.bruce-hill.com/dist";
Text_t archive_url = Texts(Text$from_str(dist_url), "/tomo@", TOMO_VERSION, "-", target, ".tar.xz");
if (!install_target) {
fprint(stderr, "The target platform \x1b[1m", target, "\x1b[m is not installed.");
if (!isatty(STDIN_FILENO))
print_err("Re-run with --install-target to download and install it from ", archive_url);
fprint_inline(stderr, "Download and install it from ", archive_url, "? [Y/n] ");
fflush(stderr);
char answer[16] = {};
if (!fgets(answer, sizeof(answer), stdin) || answer[0] == 'n' || answer[0] == 'N')
print_err("Not installing the target platform ", target);
}
print("Installing target platform \x1b[1m", target, "\x1b[m from ", archive_url, " ...");
Text_t tmp_archive = Texts(target_root, ".tar.xz.tmp");
xsystem("mkdir -p '", target_root, "'");
xsystem("curl -#fSL '", archive_url, "' -o '", tmp_archive, "'");
xsystem("tar -xJf '", tmp_archive, "' -C '", target_root, "' ./lib ./include");
xsystem("rm -f '", tmp_archive, "'");
print("Installed target platform \x1b[1m", target, "\x1b[m");
}
List_t normalize_tm_paths(List_t paths) {
Path_t cur_dir = Path$current_dir();
List_t result = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)paths.length; i++) {
Path_t path = Path$resolved(*(Path_t *)(paths.data + i * paths.stride), cur_dir);
// These commands each name a source file, so say which of the two ways
// an argument can fail to be one it is, rather than handing a directory
// to the compiler as if it were a file:
if (Path$is_directory(path, true)) fail("This is a directory, not a .tm file: ", path);
if (!Path$exists(path)) fail("path not found: ", path);
List$insert(&result, &path, I(0), sizeof(path));
}
return result;
}
void wait_for_child_success(pid_t child) {
int status;
while (waitpid(child, &status, 0) < 0 && errno == EINTR) {
if (WIFEXITED(status) || WIFSIGNALED(status)) break;
else if (WIFSTOPPED(status)) kill(child, SIGCONT);
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
_exit(WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE);
}
}
void require_writable_prefix(const char *prefix) {
// Check the nearest existing ancestor: if we can write there, we can create
// and populate the prefix subtree (bin/, man/, ...) even if it doesn't exist
// yet.
Path_t p = Path$from_str(prefix);
for (int i = 0; i < 64 && !Path$exists(p); i++) {
OptionalPath_t parent = Path$parent(p);
if (parent == NONE_PATH || strcmp(parent, p) == 0) break;
p = parent;
}
if (access(Path$as_c_string(p), W_OK) != 0)
print_err("You don't have permission to write to ", prefix,
"\nRe-run this command as the owner of that directory (for example with `sudo`).");
}
Path_t get_exe_path(Path_t path) {
ast_t *ast = parse_file(Path$as_c_string(path), NULL);
OptionalText_t exe_name = ast_metadata(ast, "EXECUTABLE");
if (exe_name.tag == TEXT_NONE) exe_name = Path$base_name(Path$with_extension(path, Text(""), true));
return Path$child(tm_build_dir(path), exe_name);
}
// Cross-compiled artifacts live in a per-target subdirectory
// (.tomo/<platform>/) so they never clobber native builds' artifacts:
Path_t build_file(Path_t path, const char *extension) {
return Path$child(tm_build_dir(path), Texts(Path$base_name(path), Text$from_str(extension)));
}
|