12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844 | // The compilation engine driving transpiling, object compilation, executable
// linking, and package builds
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <gc.h>
#include <libgen.h>
#include <miniz.h>
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "../ast.h"
#include "../compile/cli.h"
#include "../compile/files.h"
#include "../compile/headers.h"
#include "../compile/text.h"
#include "../config.h"
#include "../naming.h"
#include "../packages.h"
#include "../parse/files.h"
#include "../sha256.h"
#include "../stdlib/byte.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/enums.h"
#include "../stdlib/list.h"
#include "../stdlib/path.h"
#include "../stdlib/print.h"
#include "../stdlib/profiling.h"
#include "../stdlib/random.h"
#include "../stdlib/table.h"
#include "../stdlib/text.h"
#include "../typecheck.h"
#include "../types.h"
#include "../util.h"
#include "common.h"
#include "compilation.h"
typedef struct {
bool h : 1, c : 1, o : 1;
} staleness_t;
// The sentinel every compiled Tomo binary (and package.a) carries at the head
// of its embedded build-info blob. Scanned for both to print the info and to
// recognize a file as Tomo's own before uninstalling it.
static const char *const TOMO_BUILD_INFO_HEADER = "===== Begin Tomo Build Info =====";
// Whether a file's raw bytes contain `needle` anywhere. Used to recognize
// Tomo-produced artifacts (executables carry the build-info header; man pages
// carry TOMO_MANPAGE_MARKER) without parsing their on-disk format.
static bool file_contains(Path_t p, const char *needle) {
p = Path$expand_home(p);
int fd = open(Path$as_c_string(p), O_RDONLY);
if (fd < 0) return false;
struct stat sb;
if (fstat(fd, &sb) != 0 || sb.st_size == 0) {
close(fd);
return false;
}
char *contents = mmap(NULL, (size_t)sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
if (contents == MAP_FAILED) return false;
bool found = memmem(contents, (size_t)sb.st_size, needle, strlen(needle)) != NULL;
munmap(contents, (size_t)sb.st_size);
return found;
}
// Whether `p` is a compiled Tomo binary (carries the embedded build info).
bool is_tomo_binary(Path_t p) {
return file_contains(p, TOMO_BUILD_INFO_HEADER);
}
// Whether `p` is a man page Tomo generated (carries TOMO_MANPAGE_MARKER).
bool is_tomo_manpage(Path_t p) {
return file_contains(p, TOMO_MANPAGE_MARKER);
}
// The build-info blob lives in a named section (see compile_build_info()
// below), but rather than maintaining ELF/Mach-O/archive parsers just to find
// that section, the blob brackets itself with sentinel strings and this scans
// the raw bytes for them, which works uniformly on executables for any
// platform and on `ar` archives like package.a. Entries are NUL-separated on
// ELF (where the section is a string table) and newline-separated on Mach-O,
// so NULs print as newlines.
void print_build_info(Path_t p) {
p = Path$expand_home(p);
char *contents = NULL;
struct stat sb;
int fd = open(p, O_RDONLY);
if (fd != -1 && fstat(fd, &sb) == 0) {
contents = mmap(NULL, (size_t)sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
}
if (contents == NULL) {
fprint(stderr, "Could not open file: ", p);
exit(1);
}
const char *contents_end = contents + sb.st_size;
const char *start_header = TOMO_BUILD_INFO_HEADER;
static const char *end_header = "===== End Tomo Build Info =====";
bool found = false;
for (const char *match = contents;
(match = memmem(match, (size_t)(contents_end - match), start_header, strlen(start_header)));) {
const char *info = match + strlen(start_header);
if (info < contents_end && (*info == '\n' || *info == '\0')) info += 1;
const char *info_end = memmem(info, (size_t)(contents_end - info), end_header, strlen(end_header));
if (info_end == NULL) break;
for (const char *c = info; c < info_end; c++)
fputc(*c == '\0' ? '\n' : *c, stdout);
match = info_end + strlen(end_header);
found = true;
}
if (!found) fprint(stderr, "No Tomo build info found in: ", p);
munmap(contents, (size_t)sb.st_size);
}
// --- Embedded source zips ---------------------------------------------------
// Every compiled executable embeds a zip of the sources needed to rebuild it:
// src/ the local file tree (the program's .tm file, its transitive
// local imports, each source directory's packages.ini, every
// _embed_ed file, and every C source, header, assembly file, or
// static archive `use`d by a relative path), rooted at the
// deepest directory containing them all, so a module embedding
// ../file.txt roots the tree one level higher
// packages/ the sources of every used package, by content digest (the
// layout `tomo build --package-sources` expects); absent when
// the program uses no packages
// licenses/ Tomo's own license and each statically linked library's
// Makefile rebuilds the program with plain `make`
// README.md what the archive holds and how to rebuild it
// The zip is deterministic (fixed entry order, no timestamps: miniz is built
// with MINIZ_NO_TIME) and is bracketed by magic header/footer strings, so a
// raw scan can find it the way one finds the build info. It lives in a retained
// section (.tomo.source / __TEXT,__tomo_source), so it can also be pulled out
// with `objcopy -O binary --only-section=.tomo.source` or `segedit`.
static const char *SOURCE_ZIP_HEADER = "===== Begin Tomo Source Zip =====\n";
static const char *SOURCE_ZIP_FOOTER = "\n===== End Tomo Source Zip =====\n";
static char *slurp_file(Path_t path, size_t *size) {
int fd = open(path, O_RDONLY);
struct stat sb;
if (fd < 0) return NULL;
if (fstat(fd, &sb) != 0) {
close(fd);
return NULL;
}
char *buf = GC_MALLOC_ATOMIC((size_t)sb.st_size + 1);
for (ssize_t off = 0; off < (ssize_t)sb.st_size;) {
ssize_t got = read(fd, buf + off, (size_t)(sb.st_size - off));
if (got <= 0) {
close(fd);
return NULL;
}
off += got;
}
close(fd);
*size = (size_t)sb.st_size;
return buf;
}
static void write_all(int fd, const void *data, size_t size, Path_t path) {
for (size_t off = 0; off < size;) {
ssize_t wrote = write(fd, (const char *)data + off, size - off);
if (wrote <= 0) print_err("Could not write to file: ", path);
off += (size_t)wrote;
}
}
static int compare_source_entries(const void *a, const void *b) {
return strcmp(*(const char *const *)a, *(const char *const *)b);
}
static bool is_build_artifact(Text_t filename) {
return Text$ends_with(filename, Text(".a"), NULL) || Text$ends_with(filename, Text(".o"), NULL)
|| Text$ends_with(filename, Text(".so"), NULL) || Text$ends_with(filename, Text(".dylib"), NULL);
}
// Recursively add every file in `dir` to the zip's name->path table under
// `prefix`, skipping hidden files (like .tomo/), compiled artifacts, and
// symlinks (package binding links, since each linked package is embedded once,
// under its own packages/ entry, via the dependency graph):
static void add_dir_files(Table_t *files, Path_t dir, const char *prefix) {
OptionalList_t children = Path$glob(dir, Text("[!.]*"));
// Silently skipping an unreadable directory would put an archive together
// that is missing files without saying so:
if (children.data == NULL) print_err("Could not read the directory: ", dir);
for (int64_t i = 0; i < (int64_t)children.length; i++) {
Path_t child = *(Path_t *)(children.data + i * children.stride);
struct stat child_stat;
if (lstat(child, &child_stat) != 0 || S_ISLNK(child_stat.st_mode)) continue;
OptionalText_t base = Path$base_name(child);
if (base.tag == TEXT_NONE) print_err("This file's name is not valid UTF-8: ", child);
const char *name = String(prefix, "/", Text$as_c_string(base));
if (Path$is_directory(child, true)) add_dir_files(files, child, name);
else if (!is_build_artifact(base)) Table$str_set(files, name, Path$as_c_string(child));
}
}
static bool is_store_entry_dir(Path_t dir) {
// An entry of the active --package-sources directory counts as a store
// entry, so re-archiving a build that resolved its packages there keeps
// them content-addressed under packages/:
OptionalPath_t sources_entry = package_sources_entry(dir);
if (sources_entry != NULL && streq(sources_entry, Path$resolved(dir, Path$current_dir()))) return true;
OptionalPath_t parent = Path$parent(dir);
OptionalPath_t grandparent = parent ? Path$parent(parent) : NULL;
if (grandparent == NULL) return false; // Too close to the root to be one
return Text$equal_values(Path$base_name(parent), Text("store"))
&& Text$equal_values(Path$base_name(grandparent), Text(".tomo"));
}
// Collect the binding names and (transitively) the store-entry digests that
// the .tm files in `dir` still depend on. Parse-only, so no installs or
// confirmation prompts can trigger during garbage collection:
static void collect_needed_packages(Path_t dir, Path_t store_root, Table_t *digests, Table_t *names) {
OptionalList_t files = Path$glob(dir, Text("*.tm"));
// This drives garbage collection: reading no files here would mean nothing
// is depended on, and every installed package would be collected.
if (files.data == NULL) print_err("Could not read the directory: ", dir);
for (int64_t i = 0; i < (int64_t)files.length; i++) {
Path_t file = *(Path_t *)(files.data + i * files.stride);
// A file that doesn't parse has no `use` statements to collect, and it
// may well be unrelated to what's being built, and garbage collection
// shouldn't fail the build over it, or report an error about a file the
// user never asked to compile:
parse_error_t parse_err = {};
ast_t *ast = parse_file(Path$as_c_string(file), &parse_err);
if (!ast) continue;
for (ast_list_t *stmt = Match(ast, Block)->statements; stmt; stmt = stmt->next) {
if (stmt->ast->tag != Use) continue;
DeclareMatch(use, stmt->ast, Use);
if (use->what != USE_PACKAGE) continue;
if (names) Table$str_set(names, use->path, "");
const char *digest = find_pinned_digest(file, use->path);
if (digest == NULL) continue;
const char *hex = package_digest_hex(digest);
if (Table$str_get(*digests, hex)) continue;
Table$str_set(digests, hex, "");
Path_t entry = package_store_path(store_root, digest);
if (Path$is_directory(entry, true)) collect_needed_packages(entry, store_root, digests, NULL);
}
}
}
// Garbage-collect a source directory's .tomo/packages binding links and
// .tomo/store entries: anything no .tm file in the directory (transitively)
// uses anymore is removed. packages.ini pins and vendored sources are never
// touched, so a garbage-collected package reinstalls without any network
// access if its `use` comes back.
static void gc_package_dir(Path_t dir) {
// Store entries are content-addressed and must never be modified (and
// their dependencies live in the containing store, not their own):
if (is_store_entry_dir(dir)) return;
// The compiler's own system-wide default pins (TOMO_PATH/lib/tomo@VERSION/
// packages.ini) are shared by every project that falls back to them and
// must never be rewritten with unused=true markers based on one
// directory's usage:
Path_t tomo_lib_dir = Path$from_text(Texts(Text$from_str(TOMO_PATH), "/lib/tomo@", TOMO_VERSION));
if (Enum$equal(&dir, &tomo_lib_dir, &Path$info)) return;
Path_t tomo_root = tomo_root_for(dir);
Path_t store_root = Path$child(tomo_root, Text("store"));
Table_t digests = EMPTY_TABLE, names = EMPTY_TABLE;
collect_needed_packages(dir, store_root, &digests, &names);
mark_unused_packages(Path$child(dir, Text("packages.ini")), names);
// packages/ and store/ need not exist yet, in which case there is nothing
// to collect. Only a directory that is there but unreadable is a problem,
// and neither case is worth failing a build over.
OptionalList_t links = Path$glob(tomo_root, Text("packages/*"));
if (links.data == NULL) links = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)links.length; i++) {
Path_t link = *(Path_t *)(links.data + i * links.stride);
OptionalText_t link_name = Path$base_name(link);
// A name that isn't valid UTF-8 can't be one we created, and it can't
// be looked up either: leave it alone rather than delete it blindly.
if (link_name.tag == TEXT_NONE) continue;
if (Table$str_get(names, Text$as_c_string(link_name))) continue;
if (unlink(link) == 0)
LOG(LOG_BUILD, "Removed unused package binding: ", Path$relative_to(link, Path$current_dir()));
}
OptionalList_t entries = Path$glob(tomo_root, Text("store/*"));
if (entries.data == NULL) entries = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)entries.length; i++) {
Path_t entry = *(Path_t *)(entries.data + i * entries.stride);
OptionalText_t entry_name = Path$base_name(entry);
// As above: an undecodable name is not a digest we know, so leave it.
if (entry_name.tag == TEXT_NONE) continue;
if (Table$str_get(digests, Text$as_c_string(entry_name))) continue;
Result_t removed = Path$remove(entry, true);
if (removed.Failure.reason.tag == TEXT_NONE)
LOG(LOG_BUILD, "Removed unused store entry: ", Path$relative_to(entry, Path$current_dir()));
}
}
// Collects every `_embed_` in a module, wherever it appears (embeds are
// expressions, so they can be nested inside function bodies, type
// namespaces, etc., not just top-level statements).
static visit_behavior_t collect_embed_asts(ast_t *ast, void *userdata) {
List_t *embeds = (List_t *)userdata;
if (ast->tag == Embed) List$insert(embeds, &ast, I(0), sizeof(ast_t *));
return VISIT_PROCEED;
}
// Call `on_include` with each file `path` includes by name, resolved against
// `path`'s own directory. Quoted includes only, since <angle brackets> name
// system headers; an assembly file's `.include "..."` counts too.
static void scan_quoted_includes(Path_t path, void (*on_include)(void *userdata, Path_t included), void *userdata) {
OptionalClosure_t by_line = Path$by_line(path);
if (by_line.fn == NULL) return;
OptionalText_t (*next_line)(void *) = by_line.fn;
OptionalPath_t parent = Path$parent(path);
assert(parent); // A source file always has a directory
bool allow_dot_include = Path$has_extension(path, Text("s")) || Path$has_extension(path, Text("S"));
for (OptionalText_t line; (line = next_line(by_line.userdata)).tag != TEXT_NONE;) {
line = Text$trim(line, Text(" \t"), true, false);
if (!Text$starts_with(line, Text("#include"), NULL)
&& !(allow_dot_include && Text$starts_with(line, Text(".include"), NULL)))
continue;
// Check for `"` after `#include` or `.include` and some spaces:
if (!Text$starts_with(Text$trim(Text$from(line, I(9)), Text(" \t"), true, false), Text("\""), NULL)) continue;
List_t chunks = Text$split(line, Text("\""));
if (chunks.length < 3) // Should be `#include "foo" ...` -> ["#include ", "foo", "..."]
continue;
Text_t included = *(Text_t *)(chunks.data + 1 * chunks.stride);
on_include(userdata, Path$resolved(Path$from_text(included), parent));
}
}
static void add_included_files(Table_t *local_files, Path_t path);
static void add_included_file(void *local_files, Path_t included) {
add_included_files((Table_t *)local_files, included);
}
// Add a C source/header/assembly file to the local file set, along with
// everything it `#include "..."`s, recursively.
static void add_included_files(Table_t *local_files, Path_t path) {
path = Path$resolved(path, Path$current_dir());
if (Table$str_get(*local_files, path) != NULL) return; // Already added (and its includes walked)
if (!Path$is_file(path, true)) return; // A missing file is the compiler's error to report, not ours
Table$str_set(local_files, path, path);
// A static archive ships as the compiled binary it is; reading its bytes
// as lines of source could only turn up noise:
if (Path$has_extension(path, Text("a"))) return;
scan_quoted_includes(path, add_included_file, local_files);
}
// Add `source_file`, its packages.ini, and everything it `_embed_`s (for a
// directory embed: every file in it) to the local-file set, as resolved
// absolute paths. (Package sources don't go through this: a package is
// embedded as its whole directory, assets and all, via add_dir_files().)
static void add_local_module_files(Table_t *local_files, Path_t source_file) {
Path_t cwd = Path$current_dir();
source_file = Path$resolved(source_file, cwd);
Table$str_set(local_files, source_file, source_file);
OptionalPath_t ini = Path$sibling(source_file, Text("packages.ini"));
assert(ini); // The path names a file
if (Path$is_file(ini, true)) Table$str_set(local_files, ini, ini);
ast_t *ast = parse_file(Path$as_c_string(source_file), NULL);
if (!ast) return;
// The C sources, headers, assembly files, and static archives that this
// module `use`s by a relative path belong to the local tree too. A file
// named by an absolute path or in <angle brackets> is a system file, which
// the rebuilding machine supplies. A static archive is a compiled binary,
// but nothing in the archive could regenerate it, so it travels along --
// which is what ties the archive to the architecture it was built for:
OptionalPath_t source_dir = Path$parent(source_file);
assert(source_dir); // A source file always has a directory
for (ast_list_t *stmt = Match(ast, Block)->statements; stmt; stmt = stmt->next) {
if (stmt->ast->tag != Use) continue;
DeclareMatch(use, stmt->ast, Use);
if (use->what != USE_HEADER && use->what != USE_C_CODE && use->what != USE_ASM) continue;
if (!(starts_with(use->path, "./") || starts_with(use->path, "../"))) continue;
add_included_files(local_files, Path$resolved(Path$from_str(use->path), source_dir));
}
List_t embeds = EMPTY_LIST;
ast_visit(ast, collect_embed_asts, &embeds);
for (int64_t i = 0; i < (int64_t)embeds.length; i++) {
ast_t *embed = *(ast_t **)(embeds.data + i * embeds.stride);
Path_t embedded = get_embed_path(embed);
List_t embedded_files;
if (Path$is_directory(embedded, true)) {
embedded_files = get_dir_embed_files(embed, NULL);
} else {
embedded_files = EMPTY_LIST;
List$insert(&embedded_files, &embedded, I(0), sizeof(Path_t));
}
for (int64_t j = 0; j < (int64_t)embedded_files.length; j++) {
Path_t file = Path$resolved(*(Path_t *)(embedded_files.data + j * embedded_files.stride), cwd);
Table$str_set(local_files, file, file);
}
}
}
// Whether `dir` contains `path` (both absolute; a directory contains itself):
static bool dir_contains(Path_t dir, Path_t path) {
if (streq(dir, "/")) return path[0] == '/';
size_t len = strlen(dir);
return strncmp(path, dir, len) == 0 && (path[len] == '/' || path[len] == '\0');
}
// The deepest directory that contains both `dir` and `path` (absolute paths):
static Path_t common_ancestor_dir(Path_t dir, Path_t path) {
while (!dir_contains(dir, path)) {
OptionalPath_t parent = Path$parent(dir);
assert(parent); // "/" contains every absolute path, so the walk stops there
dir = parent;
}
return dir;
}
// The generated README.md at the archive root: what this directory is and how
// to rebuild it:
static Text_t generate_source_readme(const char *program, const char *build_command) {
return Texts("# `", program,
"` Source Code\n\n"
"This directory was extracted from a compiled Tomo binary (`tomo source ",
program,
" -o <dir>`).\n"
"It contains everything needed to rebuild that binary: the program's source\n"
"files and the sources of every package it uses. It also contains the software\n"
"licenses of code used to build the project.\n\n"
"Built with Tomo ",
TOMO_VERSION,
".\n\n"
"## Building\n\n"
"```sh\n"
"make\n"
"```\n\n"
"or directly:\n\n"
"```sh\n",
build_command,
"\n"
"```\n");
}
// Write the header + source zip + footer blob for `main_file` to `blob_path`.
// The generated README and Makefile name the program after `exe_path`, which
// `-o` can make something other than the entry file's base name:
void write_source_blob(env_t *env, Path_t main_file, Path_t exe_path, Path_t blob_path) {
Table_t dep_files = EMPTY_TABLE, to_link = EMPTY_TABLE;
build_file_dependency_graph(env->build_info, main_file, &dep_files, &to_link);
Path_t cwd = Path$current_dir();
main_file = Path$resolved(main_file, cwd);
// The local file set (absolute path -> absolute path, deduplicated):
// every non-package module, its packages.ini, and everything it embeds.
// Files inside a package store (cross builds compile installed packages'
// sources directly) get their whole store entry embedded instead:
Table_t local_files = EMPTY_TABLE;
Table_t package_dirs = EMPTY_TABLE;
for (int64_t i = 0; i < (int64_t)dep_files.entries.length; i++) {
struct {
Path_t filename;
staleness_t staleness;
} *entry = dep_files.entries.data + i * dep_files.entries.stride;
Path_t resolved = Path$resolved(entry->filename, cwd);
OptionalPath_t store_entry = package_store_entry(resolved);
if (store_entry != NULL) Table$str_set(&package_dirs, store_entry, store_entry);
else add_local_module_files(&local_files, resolved);
}
// Every installed package that gets linked in (to_link holds each one's
// package.a, including packages used transitively by other packages):
for (int64_t i = 0; i < (int64_t)to_link.entries.length; i++) {
Text_t lib = *(Text_t *)(to_link.entries.data + i * to_link.entries.stride);
if (!Text$ends_with(lib, Text("/package.a"), NULL)) continue;
OptionalPath_t pkg_dir = Path$parent(Path$resolved(Path$from_text(lib), cwd));
assert(pkg_dir); // The library path names a file
Table$str_set(&package_dirs, pkg_dir, pkg_dir);
}
// The src/ tree is rooted at the deepest directory containing every local
// file and every directory-source package:
OptionalPath_t ancestor = Path$parent(main_file);
assert(ancestor); // A source file always has a directory
for (int64_t i = 0; i < (int64_t)local_files.entries.length; i++) {
struct {
const char *key, *value;
} *entry = local_files.entries.data + i * local_files.entries.stride;
ancestor = common_ancestor_dir(ancestor, Path$from_str(entry->key));
}
for (int64_t i = 0; i < (int64_t)package_dirs.entries.length; i++) {
struct {
const char *key, *value;
} *entry = package_dirs.entries.data + i * package_dirs.entries.stride;
if (!is_store_entry_dir(Path$from_str(entry->key)))
ancestor = common_ancestor_dir(ancestor, Path$from_str(entry->key));
}
// Zip entry name -> source file path:
Table_t files = EMPTY_TABLE;
for (int64_t i = 0; i < (int64_t)local_files.entries.length; i++) {
struct {
const char *key, *value;
} *entry = local_files.entries.data + i * local_files.entries.stride;
Table$str_set(&files, String("src/", Path$relative_to(Path$from_str(entry->key), ancestor)), entry->value);
}
// Each package's full sources, and its license files:
bool has_packages = false;
for (int64_t i = 0; i < (int64_t)package_dirs.entries.length; i++) {
struct {
const char *key, *value;
} *entry = package_dirs.entries.data + i * package_dirs.entries.stride;
Path_t pkg_dir = Path$from_str(entry->key);
// Store entries go under packages/<digest>, mirroring the installed
// content-addressed layout (and matching what `--package-sources`
// expects), while a directory-source package (e.g. a vendored one) is
// part of the local file tree under src/:
const char *prefix;
if (is_store_entry_dir(pkg_dir)) {
prefix = String("packages/", Text$as_c_string(Path$base_name(pkg_dir)));
has_packages = true;
} else {
prefix = String("src/", Path$relative_to(Path$resolved(pkg_dir, Path$current_dir()), ancestor));
}
add_dir_files(&files, pkg_dir, prefix);
}
// Also include the license texts shipped with the Tomo install (Tomo's own
// license plus every statically linked library's) under licenses/:
OptionalList_t licenses =
Path$glob(Path$from_str(String(TOMO_PATH, "/share/licenses/tomo@", TOMO_VERSION)), Text("*"));
if (licenses.data == NULL) licenses = EMPTY_LIST; // An install may ship none
for (int64_t i = 0; i < (int64_t)licenses.length; i++) {
Path_t license = *(Path_t *)(licenses.data + i * licenses.stride);
Table$str_set(&files, String("licenses/", Text$as_c_string(Path$base_name(license))),
Path$as_c_string(license));
}
// Sort by entry name so the zip is deterministic:
int64_t num_files = files.entries.length;
struct {
const char *name, *path;
} *entries = GC_MALLOC((size_t)num_files * sizeof(*entries));
for (int64_t i = 0; i < num_files; i++)
memcpy(&entries[i], files.entries.data + i * files.entries.stride, sizeof(entries[i]));
qsort(entries, (size_t)num_files, sizeof(*entries), compare_source_entries);
mz_zip_archive zip = {};
if (!mz_zip_writer_init_heap(&zip, 0, 0)) print_err("Could not create source zip for ", main_file);
for (int64_t i = 0; i < num_files; i++) {
size_t size;
char *contents = slurp_file(Path$from_str(entries[i].path), &size);
if (contents == NULL) print_err("Could not read source file: ", entries[i].path);
if (!mz_zip_writer_add_mem(&zip, entries[i].name, contents, size, MZ_BEST_COMPRESSION))
print_err("Could not add ", entries[i].name, " to the source zip for ", main_file);
}
OptionalText_t base_name = Path$base_name(exe_path);
assert(base_name.tag != TEXT_NONE); // The build is about to write this file by name
const char *program = Text$as_c_string(base_name);
const char *entry_point = String("src/", Path$relative_to(main_file, ancestor));
// The Makefile runs this and the README quotes it verbatim. `-o` puts the
// executable at the archive's root rather than beside its source file:
const char *build_command = String("tomo build ", has_packages ? "--package-sources ./packages " : "", "-o ./",
program, " ./", entry_point);
const char *makefile = String("# Rebuilds the program this source archive was extracted from.\n", program, ":\n\t",
build_command, "\n\n.PHONY: ", program, "\n");
if (!mz_zip_writer_add_mem(&zip, "Makefile", makefile, strlen(makefile), MZ_BEST_COMPRESSION))
print_err("Could not add the Makefile to the source zip for ", main_file);
const char *readme = Text$as_c_string(generate_source_readme(program, build_command));
if (!mz_zip_writer_add_mem(&zip, "README.md", readme, strlen(readme), MZ_BEST_COMPRESSION))
print_err("Could not add the README to the source zip for ", main_file);
void *zip_data;
size_t zip_size;
if (!mz_zip_writer_finalize_heap_archive(&zip, &zip_data, &zip_size))
print_err("Could not finalize the source zip for ", main_file);
mz_zip_writer_end(&zip);
int fd = open(blob_path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd < 0) print_err("Could not write source zip: ", blob_path);
write_all(fd, SOURCE_ZIP_HEADER, strlen(SOURCE_ZIP_HEADER), blob_path);
write_all(fd, zip_data, zip_size, blob_path);
write_all(fd, SOURCE_ZIP_FOOTER, strlen(SOURCE_ZIP_FOOTER), blob_path);
close(fd);
mz_free(zip_data);
}
// The source zip blob inside a compiled binary: the mapped file, plus where
// the zip's bytes start and end within it.
typedef struct {
Path_t binary;
char *mapped;
size_t mapped_size;
const char *zip_start;
size_t zip_size;
} source_blob_t;
// Locate the source zip embedded in `binary`, failing if there isn't a valid
// one. Release it with release_source_blob().
static source_blob_t find_embedded_source(Path_t binary) {
binary = Path$resolved(Path$expand_home(binary), Path$current_dir());
char *contents = NULL;
struct stat sb;
int fd = open(binary, O_RDONLY);
if (fd != -1 && fstat(fd, &sb) == 0) {
contents = mmap(NULL, (size_t)sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
}
if (contents == NULL) print_err("Could not open file: ", binary);
const char *zip_start = memmem(contents, (size_t)sb.st_size, SOURCE_ZIP_HEADER, strlen(SOURCE_ZIP_HEADER));
if (zip_start == NULL) print_err("No embedded Tomo source found in: ", binary);
zip_start += strlen(SOURCE_ZIP_HEADER);
const char *zip_end =
memmem(zip_start, (size_t)(contents + sb.st_size - zip_start), SOURCE_ZIP_FOOTER, strlen(SOURCE_ZIP_FOOTER));
if (zip_end == NULL) print_err("The embedded Tomo source in ", binary, " is truncated");
return (source_blob_t){.binary = binary,
.mapped = contents,
.mapped_size = (size_t)sb.st_size,
.zip_start = zip_start,
.zip_size = (size_t)(zip_end - zip_start)};
}
static void release_source_blob(source_blob_t blob) {
munmap(blob.mapped, blob.mapped_size);
}
static void open_source_zip(source_blob_t blob, mz_zip_archive *zip) {
memset(zip, 0, sizeof(*zip));
if (!mz_zip_reader_init_mem(zip, blob.zip_start, blob.zip_size, 0))
print_err("The embedded Tomo source in ", blob.binary, " is not a valid zip file");
}
// Print what the source archive embedded in `binary` holds, writing nothing:
void list_embedded_source(Path_t binary) {
source_blob_t blob = find_embedded_source(binary);
mz_zip_archive zip;
open_source_zip(blob, &zip);
int64_t total_files = 0, total_bytes = 0;
for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); i++) {
mz_zip_archive_file_stat entry;
if (!mz_zip_reader_file_stat(&zip, i, &entry))
print_err("The embedded Tomo source in ", binary, " is not a valid zip file");
if (entry.m_is_directory) continue;
total_files += 1;
total_bytes += (int64_t)entry.m_uncomp_size;
Text_t size_text = Text$from_str(String((int64_t)entry.m_uncomp_size));
print(Text$left_pad(size_text, I(10), Text(" "), Text("C")), " ", entry.m_filename);
}
print("\n", total_files, total_files == 1 ? " file, " : " files, ", total_bytes, " bytes uncompressed (",
(int64_t)blob.zip_size, " bytes embedded in the binary)");
// Each command sits alone on its line, unquoted, and names the binary the
// way the caller named it, so it can be copied and run as it stands:
print("\nTo unpack these files to a directory, run:\n"
" tomo source ",
binary,
" -o <dir>\n"
"\n"
"To write the archive itself out as a zip file, run:\n"
" tomo source ",
binary, " -o <file>.zip");
mz_zip_reader_end(&zip);
release_source_blob(blob);
}
// Write the source archive embedded in `binary` to `destination` as a zip
// file, verbatim -- the bytes are already a zip inside the binary:
void save_embedded_source_zip(Path_t binary, Path_t destination) {
source_blob_t blob = find_embedded_source(binary);
mz_zip_archive zip; // Opened only to reject a corrupt archive before writing it out
open_source_zip(blob, &zip);
mz_zip_reader_end(&zip);
destination = Path$resolved(Path$expand_home(destination), Path$current_dir());
int out_fd = open(destination, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (out_fd < 0) print_err("Could not write ", destination);
write_all(out_fd, blob.zip_start, blob.zip_size, destination);
close(out_fd);
release_source_blob(blob);
print("Wrote ", Path$relative_to(destination, Path$current_dir()), " (", (int64_t)blob.zip_size, " bytes)");
}
// Unpack the source archive embedded in a compiled binary into `destination`:
void extract_embedded_source(Path_t binary, Path_t destination) {
source_blob_t blob = find_embedded_source(binary);
binary = blob.binary;
mz_zip_archive zip;
open_source_zip(blob, &zip);
Path_t outdir = Path$resolved(Path$expand_home(destination), Path$current_dir());
for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); i++) {
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&zip, i, &stat))
print_err("The embedded Tomo source in ", binary, " is not a valid zip file");
if (stat.m_filename[0] == '/' || strstr(stat.m_filename, "..")) {
fprint(stderr, "Skipping unsafe path in embedded source: ", stat.m_filename);
continue;
}
if (stat.m_is_directory) continue;
size_t size;
void *data = mz_zip_reader_extract_to_heap(&zip, i, &size, 0);
if (data == NULL) print_err("Could not extract ", stat.m_filename, " from the source zip in ", binary);
Path_t out = Path$child(outdir, Text$from_str(stat.m_filename));
OptionalPath_t out_dir = Path$parent(out);
assert(out_dir); // The output path names a file
Path$create_directory(out_dir, 0755, true);
int out_fd = open(out, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (out_fd < 0) print_err("Could not write extracted file: ", out);
write_all(out_fd, data, size, out);
close(out_fd);
mz_free(data);
print("Extracted ", Path$relative_to(out, Path$current_dir()));
}
mz_zip_reader_end(&zip);
release_source_blob(blob);
print("\nThis is a self-contained source archive: see its README.md, or run `make`\n"
"inside it to rebuild the program without fetching anything.");
}
// The C code embedding the source blob in a retained section on ELF targets
// (`.incbin` splices the blob file in verbatim). On Mach-O the blob is
// embedded at link time instead, via -sectcreate (see compile_executable):
static Text_t compile_source_asm(Path_t blob_path) {
Text_t asm_text =
Texts(".pushsection .tomo.source,\"aR\",%progbits\n"
".globl tomo_source\ntomo_source:\n"
".incbin ",
Text$quoted(Text$from_str(Path$as_c_string(blob_path)), false, Text("\"")), "\n.popsection\n");
return Texts("__asm__(", Text$quoted(asm_text, false, Text("\"")), ");\n");
}
// The C code defining the build-info blob, which lives in a named section so
// it can be retrieved with standard tools (readelf -p .tomo.build_info /
// otool -s __TEXT __tomo_build) in addition to `tomo --build-info`'s
// sentinel-based scan. On ELF targets the section is emitted with module-level
// asm so it can carry the SHF_STRINGS + SHF_GNU_RETAIN flags ("aRS"): one
// NUL-terminated string per entry, which `readelf -p` prints one line at a
// time, retained through the linker's default --gc-sections. Mach-O has no
// equivalent flags, so there the blob is a plain newline-separated char array.
static Text_t compile_build_info(env_t *env, const char *symbol) {
if (link_macho) {
Text_t blob = Text("===== Begin Tomo Build Info =====\n");
for (int64_t i = 0; i < (int64_t)env->build_info->entries.length; i++) {
struct {
const char *key, *value;
} *entry = env->build_info->entries.data + i * env->build_info->entries.stride;
blob = Texts(blob, entry->key, ": ", entry->value, "\n");
}
blob = Texts(blob, "===== End Tomo Build Info =====\n");
return Texts("const char ", symbol,
"[] __attribute__((used, visibility(\"default\"), section(\"__TEXT,__tomo_build\"))) = ",
Text$quoted(blob, false, Text("\"")), ";\n");
}
Text_t asm_text = Texts(".pushsection .tomo.build_info,\"aRS\",%progbits\n"
".globl ",
symbol, "\n", symbol,
":\n"
".asciz \"===== Begin Tomo Build Info =====\"\n");
for (int64_t i = 0; i < (int64_t)env->build_info->entries.length; i++) {
struct {
const char *key, *value;
} *entry = env->build_info->entries.data + i * env->build_info->entries.stride;
asm_text =
Texts(asm_text, ".asciz ", Text$quoted(Texts(entry->key, ": ", entry->value), false, Text("\"")), "\n");
}
asm_text = Texts(asm_text, ".asciz \"===== End Tomo Build Info =====\"\n.popsection\n");
return Texts("__asm__(", Text$quoted(asm_text, false, Text("\"")), ");\n");
}
// Launch a shell command with its stdout on a pipe (logging it like
// command_output does), but WITHOUT waiting for it, so several can run
// concurrently rather than one-at-a-time:
#define popen_logged(...) \
({ \
const char *_cmd = String(__VA_ARGS__); \
LOG(LOG_COMMANDS, "\033[94;1m", _cmd, "\033[m"); \
popen(_cmd, "r"); \
})
// Read one newline-trimmed line from a pipe, or NULL at EOF (e.g. when the
// command produced no output because it failed):
static char *read_pipe_line(FILE *f) {
if (!f) return NULL;
char *buf = GC_MALLOC_ATOMIC(1024);
if (!fgets(buf, 1023, f)) return NULL;
size_t n = strlen(buf);
if (n > 0 && buf[n - 1] == '\n') buf[n - 1] = '\0';
return buf;
}
static void add_git_info(env_t *env, Path_t dir) {
// Launch both git queries up front so they run in parallel (each popen
// spawns a shell + git). The commit hash and its time come from a single
// `git log` (two output lines) instead of separate rev-parse + log calls,
// so this is 2 concurrent invocations rather than 3 sequential ones:
FILE *log = popen_logged("git -C '", dir, "' log -1 --format='%H%n%cI' 2>/dev/null");
FILE *dirty = popen_logged("git -C '", dir, "' diff --quiet 2>/dev/null && echo false || echo true");
char *commit = read_pipe_line(log);
char *commit_time = commit ? read_pipe_line(log) : NULL;
char *local_changes = read_pipe_line(dirty);
if (log) pclose(log);
if (dirty) pclose(dirty);
// No commit line means this isn't a git repo (or git is unavailable); the
// `diff` result is meaningless then, so drop it too:
if (!commit) return;
Table$str_set(env->build_info, "Git commit", commit);
if (commit_time) Table$str_set(env->build_info, "Git commit time", commit_time);
if (local_changes) Table$str_set(env->build_info, "Git local changes", local_changes);
}
void build_package(Path_t pkg_dir) {
pkg_dir = Path$resolved(pkg_dir, Path$current_dir());
if (!Path$is_directory(pkg_dir, true)) print_err("Not a valid directory: ", pkg_dir);
OptionalList_t tm_files = Path$glob(pkg_dir, Text("[!._0-9]*.tm"));
if (tm_files.data == NULL) print_err("Could not read the package directory: ", pkg_dir);
// Cross-compiled package archives go in the per-target .tomo directory so
// they don't clobber the native package.a:
Path_t archive = cross_compiling ? build_file(Path$child(pkg_dir, Text("package.a")), "")
: Path$child(pkg_dir, Text("package.a"));
build_package_archive(pkg_dir, tm_files, archive);
}
void build_package_archive(Path_t pkg_dir, List_t tm_files, Path_t archive) {
env_t *env = fresh_scope(global_env(source_mapping, instrument, debugging));
List_t object_files = EMPTY_LIST, extra_ldlibs = EMPTY_LIST;
compile_files(env, tm_files, &object_files, &extra_ldlibs, COMPILE_OBJ);
if (is_stale_for_any(archive, object_files, false)) {
add_git_info(env, pkg_dir);
// Store metadata about the package's build information (in the
// package's own .tomo directory, not the working directory's):
Path_t build_info_obj = build_file(Path$child(pkg_dir, Text("__build_info")), ".o");
{
FILE *prog = run_cmd(cc, " ", cflags, " -x c -c - -o ", build_info_obj);
if (!prog) print_err("Failed to run C compiler: ", cc);
Text_t build_info = compile_build_info(env, "package_build_info");
fputs(Text$as_c_string(build_info), prog);
int status = pclose(prog);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) exit(EXIT_FAILURE);
}
FILE *prog = run_cmd(ar, " rcs '", archive, "' ", paths_str(object_files), " '", build_info_obj, "'");
if (!prog) print_err("Failed to run `ar`");
int status = pclose(prog);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) exit(EXIT_FAILURE);
LOG(LOG_BUILD, "Compiled static package:\t", Path$relative_to(archive, Path$current_dir()));
gc_package_dir(pkg_dir);
} else {
LOG(LOG_SKIP, "Unchanged: ", archive);
}
}
// ---- atomic build artifacts -----------------------------------------------
//
// Artifacts are keyed by source path, so two `tomo` processes only ever write
// the same file when they're building a shared dependency, but when they do,
// an O_TRUNC-then-write leaves a window where a third process reads a truncated
// header or links a half-written object. Every artifact is therefore written to
// a sibling temp file and renamed into place: rename(2) within a directory is
// atomic, so a reader sees the old artifact or the new one, never a partial
// one. It also means an interrupted build can't leave behind a corrupt file
// that a later staleness check mistakes for a good one.
static Path_t artifact_temp(Path_t final) {
return (Path_t)String(final, ".tmp", (int64_t)getpid());
}
static void commit_artifact(Path_t temp, Path_t final) {
if (rename(Path$as_c_string(temp), Path$as_c_string(final)) != 0) {
unlink(Path$as_c_string(temp));
print_err("Failed to write ", final, ": ", strerror(errno));
}
}
// --- Precompiled header -----------------------------------------------------
// Every generated .c opens with `#include <tomo.h>`, which pulls in ~40 more
// headers. Preprocessing and parsing them is a fixed per-translation-unit cost
// that dwarfs the code Tomo actually generates: measured on
// examples/learnxiny.tm, preloading a precompiled tomo.h instead cuts the
// object compile from 136ms to 119ms at -O0, and from 228ms to 218ms at -O3
// (where the optimizer, which a PCH does nothing for, dominates).
//
// A PCH is only usable by an invocation whose language and codegen options
// match the ones it was built with (clang rejects a mismatch outright rather
// than falling back), and Tomo's flags vary with the optimization level, the
// target platform, and which headers the installation has. So rather than
// shipping prebuilt ones (which cross-compiled distribution archives couldn't
// produce anyway: their `tomo` doesn't run on the build host), the PCH is
// built on demand and cached under a fingerprint covering everything that
// could invalidate it. A miss costs one extra ~65ms compile, once.
// Append each header matched by `pattern` to `fingerprint` with its size and
// mtime, so that reinstalling Tomo (or editing a header in place) names a
// different cache entry instead of tripping clang's "file has been modified
// since the precompiled header was built" error.
static const char *stamp_headers(const char *fingerprint, Path_t dir, Text_t pattern) {
OptionalList_t files = Path$glob(dir, pattern);
// A missing include directory contributes nothing to the fingerprint,
// which is safe: the key is only ever weaker, never wrong.
if (files.data == NULL) return fingerprint;
for (int64_t i = 0; i < (int64_t)files.length; i++) {
Path_t file = *(Path_t *)(files.data + i * files.stride);
struct stat sb;
if (stat(Path$as_c_string(file), &sb) != 0) continue;
fingerprint = String(fingerprint, "\n", file, ":", (int64_t)sb.st_size, ":", (int64_t)sb.st_mtime);
}
return fingerprint;
}
// Each reinstall of Tomo (or change of optimization level or target platform)
// names a new cache entry, and nothing else ever removes the old one, so keep
// only the most recently built few. Called on a cache miss, which is the only
// time the directory grows. The cap is generous enough for the levels and
// targets one checkout realistically compiles at.
#define MAX_CACHED_PCHS 8
static void prune_pch_cache(Path_t cache_dir, Path_t keep) {
OptionalList_t cached = Path$glob(cache_dir, Text("*.pch"));
if (cached.data == NULL) return; // No cache directory yet: nothing to prune
if ((int64_t)cached.length <= MAX_CACHED_PCHS) return;
// Drop the oldest entries first: repeatedly evict the least recently
// modified one that isn't the entry just built.
for (int64_t remaining = (int64_t)cached.length; remaining > MAX_CACHED_PCHS; remaining--) {
Path_t oldest = NONE_PATH;
time_t oldest_time = 0;
for (int64_t i = 0; i < (int64_t)cached.length; i++) {
Path_t file = *(Path_t *)(cached.data + i * cached.stride);
struct stat sb;
if (file == NONE_PATH || streq(file, keep) || stat(Path$as_c_string(file), &sb) != 0) continue;
if (oldest == NONE_PATH || sb.st_mtime < oldest_time) {
oldest = file;
oldest_time = sb.st_mtime;
}
}
if (oldest == NONE_PATH) break;
unlink(Path$as_c_string(oldest));
// Tombstone it so the next pass doesn't pick it again:
for (int64_t i = 0; i < (int64_t)cached.length; i++) {
Path_t *file = (Path_t *)(cached.data + i * cached.stride);
if (*file != NONE_PATH && streq(*file, oldest)) *file = NONE_PATH;
}
}
}
// The cached PCH to preload, or NONE if there isn't one to use. Memoized: the
// answer is the same for every translation unit in a build.
static OptionalPath_t precompiled_header = NONE_PATH;
static bool precompiled_header_resolved = false;
// A -D carrying the cache fingerprint, passed to both the PCH build and every
// compile that preloads it (they must agree on macro definitions or clang
// rejects the PCH). It exists to defeat `zig cc`'s own compilation cache,
// which is content-addressed: without it, zig answers a PCH build with an
// artifact it produced earlier from byte-identical headers, one that recorded
// their *previous* mtimes, and clang validates a PCH by mtime, so the reused
// artifact is rejected by every compile that tries to load it. Since the
// fingerprint changes whenever those mtimes do (a plain `cp` of the headers
// during `make install` is enough), keying zig's cache on it too keeps the two
// caches from disagreeing.
static const char *precompiled_header_stamp = "";
static OptionalPath_t get_precompiled_header(void) {
if (precompiled_header_resolved) return precompiled_header;
precompiled_header_resolved = true;
// Escape hatch: a user whose install has drifted (hand-edited headers, a
// partially-overwritten prefix) can turn the PCH off without reinstalling.
const char *disabled = getenv("TOMO_NO_PCH");
if (disabled && disabled[0] != '\0' && !streq(disabled, "0")) return NONE_PATH;
Path_t include_dir = Path$from_str(String(lib_root, "/include/tomo@", TOMO_VERSION));
Path_t umbrella = Path$child(include_dir, Text("tomo.h"));
if (!Path$is_file(umbrella, true)) return NONE_PATH;
// Everything that decides whether a given PCH is valid for this compile:
// the exact invocation, and the headers it would be built from.
const char *fingerprint = String(cc, " ", cflags, " -O", optimization);
fingerprint = stamp_headers(fingerprint, include_dir, Text("*.h"));
// One level down covers every subdirectory tomo.h reaches into: tomo/ for
// the standard library's own headers, plus gc/ and unistring/, which the
// vendored gc.h and uni*.h pull in and which clang validates just as
// strictly as the rest.
fingerprint = stamp_headers(fingerprint, include_dir, Text("*/*.h"));
// 64 bits of the digest is plenty to keep configurations apart, the same
// truncation tomo_root_for() uses to name build-cache directories.
// siphash24() would not do here: its key is randomized per process, so it
// would name a different file every run.
char digest[SHA256_HEX_SIZE];
sha256_hex(fingerprint, strlen(fingerprint), digest);
digest[16] = '\0';
const char *name = String("tomo-", digest, ".pch");
// An identifier, not a number: the value is never expanded, and a bare hex
// digest would be a pp-number too wide for any integer type.
precompiled_header_stamp = String(" -D__TOMO_PCH__=pch_", digest);
Path_t cache_dir = Path$child(xdg_tomo_dir("XDG_CACHE_HOME", "~/.cache"), Text("pch"));
Path_t pch = Path$child(cache_dir, Text$from_str(name));
if (Path$is_file(pch, true)) {
precompiled_header = pch;
return precompiled_header;
}
// Cache miss: build it. A failure here is not fatal, since the same
// headers are about to be compiled the ordinary way, which will report any
// real error against the user's own source rather than against tomo.h.
(void)Path$create_directory(cache_dir, 0755, /*recursive=*/true);
Path_t temp = artifact_temp(pch);
TOMO_PROFILE_SPAN_BEGIN(span, "cc precompile tomo.h");
FILE *prog = run_cmd(cc, " ", cflags, " -O", optimization, precompiled_header_stamp, " -x c-header ", umbrella,
" -o ", temp);
int status = prog ? pclose(prog) : -1;
TOMO_PROFILE_SPAN_END(span);
if (!prog || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
unlink(Path$as_c_string(temp));
return NONE_PATH;
}
commit_artifact(temp, pch);
LOG(LOG_BUILD, "Precompiled header:\t", pch);
prune_pch_cache(cache_dir, pch);
precompiled_header = pch;
return precompiled_header;
}
// Build the precompiled header now, if it isn't cached already. Callers that
// fork a worker per file use this to make sure the one shared PCH is built
// once in the parent instead of N times in parallel children.
void warm_precompiled_header(void) {
(void)get_precompiled_header();
}
// Compile one translation unit: `cc <cflags> -O<level> [pch] <args>`. Returns
// whether it succeeded.
//
// clang refuses a precompiled header whose input files have changed since it
// was built, and that can happen underneath a build in progress: the cache
// entry is chosen from the headers' sizes and mtimes, but a `make install` (or
// anything else that rewrites the installed headers) landing between that
// check and the moment clang reads them leaves the entry stale. A stale cache
// must never be able to fail a compile that would otherwise succeed, so an
// attempt that used a PCH holds its diagnostics back; if it fails, the PCH is
// discarded and the compile re-run without one, which either succeeds or
// reports the program's real error itself, uncaptured and in colour.
static bool run_compile(const char *args, Path_t scratch) {
OptionalPath_t pch_path = get_precompiled_header();
const char *pch = pch_path == NONE_PATH ? "" : String(precompiled_header_stamp, " -include-pch '", pch_path, "'");
if (pch[0] != '\0') {
Path_t captured = (Path_t)String(scratch, ".err");
FILE *prog = run_cmd(cc, " ", cflags, " -O", optimization, pch, " ", args, " 2>'", captured, "'");
int status = prog ? pclose(prog) : -1;
if (prog && WIFEXITED(status) && WEXITSTATUS(status) == 0) {
// It worked, so anything it printed is a warning worth showing:
FILE *diagnostics = fopen(Path$as_c_string(captured), "r");
if (diagnostics) {
for (int c; (c = fgetc(diagnostics)) != EOF;)
fputc(c, stderr);
fclose(diagnostics);
}
unlink(Path$as_c_string(captured));
return true;
}
unlink(Path$as_c_string(captured));
// Throw the stale PCH away so the retry below, and every later
// compile in this run, goes without one:
if (precompiled_header != NONE_PATH) unlink(Path$as_c_string(precompiled_header));
precompiled_header = NONE_PATH;
precompiled_header_resolved = true;
}
FILE *prog = run_cmd(cc, " ", cflags, " -O", optimization, " ", args);
if (!prog) print_err("Failed to run C compiler: ", cc);
int status = pclose(prog);
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
void compile_files(env_t *env, List_t to_compile, List_t *object_files, List_t *extra_ldlibs, compile_mode_t mode) {
Table_t to_link = EMPTY_TABLE;
Table_t dependency_files = EMPTY_TABLE;
for (int64_t i = 0; i < (int64_t)to_compile.length; i++) {
Path_t filename = *(Path_t *)(to_compile.data + i * to_compile.stride);
if (!Path$has_extension(filename, Text("tm")))
print_err("Not a valid .tm file: \x1b[91;1m", filename, "\x1b[m");
if (!Path$is_file(filename, true)) print_err("Couldn't find file: ", filename);
TOMO_PROFILE_SPAN("dependency graph",
build_file_dependency_graph(env->build_info, filename, &dependency_files, &to_link));
}
// Make sure all files and dependencies have a .id file:
for (int64_t i = 0; i < (int64_t)dependency_files.entries.length; i++) {
struct {
Path_t filename;
staleness_t staleness;
} *entry = (dependency_files.entries.data + i * dependency_files.entries.stride);
Path_t id_file = build_file(entry->filename, ".id");
if (Path$exists(id_file)) continue;
static const char id_chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
int64_t num_id_chars = (int64_t)strlen(id_chars);
char id_str[8];
for (int j = 0; j < (int)sizeof(id_str); j++) {
id_str[j] = id_chars[random_range(0, num_id_chars - 1)];
}
Text_t filename_id = Text("");
OptionalText_t base = Path$base_name(entry->filename);
if (base.tag == TEXT_NONE) print_err("This file's name is not valid UTF-8: ", entry->filename);
TextIter_t state = NEW_TEXT_ITER_STATE(base);
for (int64_t j = 0; j < (int64_t)base.length; j++) {
uint32_t c = Text$get_main_grapheme_fast(&state, j);
if (c == '.') break;
if (isalpha(c) || isdigit(c) || c == '_')
filename_id = Texts(filename_id, Text$from_strn((char[]){(char)c}, 1));
}
// The id is *random*, so two processes racing here would invent two different ones and each bake its own
// into the names it mangles, a wrong build and not just a duplicated one. Write the id to a temp file and
// link() it into place: link is atomic and fails with EEXIST if someone else got there first, so everyone
// ends up agreeing on a single id. Linking after the content is written means a reader never sees a
// half-written id either.
Path_t id_temp = artifact_temp(id_file);
Path$write(id_temp, Texts(filename_id, Text("_"), Text$from_strn(id_str, sizeof(id_str))), 0644);
if (link(Path$as_c_string(id_temp), Path$as_c_string(id_file)) != 0 && errno != EEXIST)
print_err("Failed to create id file: ", id_file, ": ", Text$from_str(strerror(errno)));
unlink(Path$as_c_string(id_temp));
}
// (Re)compile header files, eagerly for explicitly passed in files, lazily
// for downstream dependencies:
for (int64_t i = 0; i < (int64_t)dependency_files.entries.length; i++) {
struct {
Path_t filename;
staleness_t staleness;
} *entry = (dependency_files.entries.data + i * dependency_files.entries.stride);
if (entry->staleness.h || clean_build) {
TOMO_PROFILE_SPAN("transpile header", transpile_header(env, entry->filename));
entry->staleness.o = true;
} else {
LOG(LOG_SKIP, "Unchanged: ", build_file(entry->filename, ".h"));
}
}
env->imports = new (Table_t);
struct child_s {
struct child_s *next;
pid_t pid;
} *child_processes = NULL;
// Transpiling and `zig cc -c` run in forked children, so their profile
// spans never reach the parent's totals. When profiling, give the children
// a pipe to serialize their spans back over (merged into the parent below).
int profile_pipe[2] = {-1, -1};
if (profiling && pipe(profile_pipe) != 0) profile_pipe[0] = profile_pipe[1] = -1;
// Resolve (and, on a cold cache, build) the precompiled header here in the
// parent: the children below all need the same one, and each forking its
// own build would mean N redundant compiles of tomo.h. Only when there is
// actually something to compile, though: an up-to-date build that forks
// no children at all shouldn't pay for a cold cache.
if (mode != COMPILE_C_FILES) {
for (int64_t i = 0; i < (int64_t)dependency_files.entries.length; i++) {
struct {
Path_t filename;
staleness_t staleness;
} *entry = (dependency_files.entries.data + i * dependency_files.entries.stride);
if (clean_build || entry->staleness.c || entry->staleness.h || entry->staleness.o
|| is_config_outdated(entry->filename)) {
warm_precompiled_header();
break;
}
}
}
// Drain the parent's stdio buffers before forking. When output is a pipe
// rather than a tty it is block-buffered, so anything still buffered here
// would be inherited by every child and printed a second time when they
// flush on exit.
fflush(NULL);
// (Re)transpile and compile object files, eagerly for files explicitly
// specified and lazily for downstream dependencies:
for (int64_t i = 0; i < (int64_t)dependency_files.entries.length; i++) {
struct {
Path_t filename;
staleness_t staleness;
} *entry = (dependency_files.entries.data + i * dependency_files.entries.stride);
// A stale config means retranspiling, not just recompiling: it covers
// flags that change the generated code (--instrument, --source-mapping),
// not only the ones passed to the C compiler.
bool config_outdated = is_config_outdated(entry->filename);
if (!clean_build && !entry->staleness.c && !entry->staleness.h && !entry->staleness.o && !config_outdated) {
LOG(LOG_SKIP, "Unchanged: ", build_file(entry->filename, ".c"));
LOG(LOG_SKIP, "Unchanged: ", build_file(entry->filename, ".o"));
continue;
}
pid_t pid = fork();
if (pid == 0) {
// Start with a clean slate so we report only this child's work, not
// the parent history inherited by copy-on-write:
tomo_profile_reset();
if (clean_build || entry->staleness.c || config_outdated)
TOMO_PROFILE_SPAN("transpile code", transpile_code(env, entry->filename));
else LOG(LOG_SKIP, "Unchanged: ", build_file(entry->filename, ".c"));
if (mode != COMPILE_C_FILES) TOMO_PROFILE_SPAN("cc compile object", compile_object_file(entry->filename));
// `tomo transpile` writes a .c but no .o, so it never refreshes the
// .config that says which flags that .c was generated with. Drop
// the stale one instead, or a later `tomo build` would see an
// up-to-date .c with a matching .config and reuse code generated
// under different flags (e.g. a --instrument transpile leaking
// instrumentation into a plain build):
else unlink(Path$as_c_string(build_file(entry->filename, ".config")));
if (profile_pipe[1] >= 0) tomo_profile_serialize(profile_pipe[1]);
fflush(NULL);
_exit(EXIT_SUCCESS);
}
child_processes = new (struct child_s, .next = child_processes, .pid = pid);
}
// Close the parent's write end so the merge read below sees EOF once every
// child has written and exited:
if (profile_pipe[1] >= 0) close(profile_pipe[1]);
for (; child_processes; child_processes = child_processes->next)
wait_for_child_success(child_processes->pid);
if (profile_pipe[0] >= 0) tomo_profile_merge(profile_pipe[0]); // closes the read end
if (object_files) {
for (int64_t i = 0; i < (int64_t)dependency_files.entries.length; i++) {
struct {
Path_t filename;
staleness_t staleness;
} *entry = (dependency_files.entries.data + i * dependency_files.entries.stride);
Path_t path = entry->filename;
path = build_file(path, ".o");
List$insert(object_files, &path, I(0), sizeof(Path_t));
}
}
if (extra_ldlibs) {
for (int64_t i = 0; i < (int64_t)to_link.entries.length; i++) {
Text_t lib = *(Text_t *)(to_link.entries.data + i * to_link.entries.stride);
List$insert(extra_ldlibs, &lib, I(0), sizeof(Text_t));
}
}
}
bool is_config_outdated(Path_t path) {
OptionalText_t config = Path$read(build_file(path, ".config"));
if (config.tag == TEXT_NONE) return true;
return !Text$equal_values(config, config_summary);
}
void build_file_dependency_graph(Table_t *build_info, Path_t path, Table_t *to_compile, Table_t *to_link) {
if (Table$has_value(*to_compile, path, Table$info(&Path$info, &Byte$info))) return;
OptionalPath_t packages_ini = Path$sibling(path, Text("packages.ini"));
assert(packages_ini); // A source file always has a directory
staleness_t staleness = {
.h = is_stale(build_file(path, ".h"), packages_ini, true) || is_stale(build_file(path, ".h"), path, false)
|| is_stale(build_file(path, ".h"), build_file(path, ".id"), false),
.c = is_stale(build_file(path, ".c"), packages_ini, true) || is_stale(build_file(path, ".c"), path, false)
|| is_stale(build_file(path, ".c"), build_file(path, ".id"), false),
};
staleness.o = staleness.c || staleness.h || is_stale(build_file(path, ".o"), build_file(path, ".c"), false)
|| is_stale(build_file(path, ".o"), build_file(path, ".h"), false);
Table$set(to_compile, &path, &staleness, Table$info(&Path$info, &Byte$info));
OptionalText_t path_extension = Path$extension(path, true);
assert(path_extension.tag != TEXT_NONE && Text$equal_values(path_extension, Text("tm")));
ast_t *ast = parse_file(Path$as_c_string(path), NULL);
if (!ast) print_err("Could not parse file: ", path);
// An `_embed_`ed file's bytes are baked into the transpiled C, and its
// contents can even change the header (whether a file-scope embed is a
// compile-time constant depends on the bytes, see embed_is_constant()),
// so embeds are build dependencies just like local imports. A directory
// embed depends on every file it embeds, and on every directory in its
// tree: a directory's mtime is what changes when a file is added,
// removed, or renamed, which no surviving file's mtime reflects.
List_t embeds = EMPTY_LIST;
ast_visit(ast, collect_embed_asts, &embeds);
for (int64_t i = 0; i < (int64_t)embeds.length; i++) {
ast_t *embed = *(ast_t **)(embeds.data + i * embeds.stride);
Path_t embedded = get_embed_path(embed);
List_t embed_deps = EMPTY_LIST;
if (Path$is_directory(embedded, true)) {
List$insert(&embed_deps, &embedded, I(0), sizeof(Path_t));
List_t dir_files = get_dir_embed_files(embed, &embed_deps);
List$insert_all(&embed_deps, dir_files, I(0), sizeof(Path_t));
} else if (Path$is_file(embedded, true)) {
List$insert(&embed_deps, &embedded, I(0), sizeof(Path_t));
} else {
// A missing embed forces a retranspile, so the compile stage
// reports it with a proper source location instead of erroring here:
staleness.h = staleness.c = true;
continue;
}
for (int64_t j = 0; j < (int64_t)embed_deps.length; j++) {
Path_t dep = *(Path_t *)(embed_deps.data + j * embed_deps.stride);
if (is_stale(build_file(path, ".h"), dep, false)) staleness.h = true;
if (is_stale(build_file(path, ".c"), dep, false)) staleness.c = true;
}
}
if (staleness.c || staleness.h) {
staleness.o = true;
Table$set(to_compile, &path, &staleness, Table$info(&Path$info, &Byte$info));
}
for (ast_list_t *stmt = Match(ast, Block)->statements; stmt; stmt = stmt->next) {
ast_t *stmt_ast = stmt->ast;
if (stmt_ast->tag != Use) continue;
DeclareMatch(use, stmt_ast, Use);
switch (use->what) {
case USE_LOCAL: {
OptionalPath_t source_dir = Path$parent(path);
assert(source_dir); // A source file always has a directory
Path_t dep_tm = Path$resolved(Path$from_str(use->path), source_dir);
if (!Path$is_file(dep_tm, true)) code_err(stmt_ast, "Not a valid file: ", dep_tm);
if (is_stale(build_file(path, ".h"), dep_tm, false)) staleness.h = true;
if (is_stale(build_file(path, ".c"), dep_tm, false)) staleness.c = true;
if (staleness.c || staleness.h) staleness.o = true;
Table$set(to_compile, &path, &staleness, Table$info(&Path$info, &Byte$info));
build_file_dependency_graph(build_info, dep_tm, to_compile, to_link);
break;
}
case USE_PACKAGE: {
OptionalPath_t installed = find_installed_package(build_info, stmt_ast);
if (!installed) code_err(stmt_ast, "I don't know where to find this package.");
OptionalList_t children = Path$glob(installed, Text("[!._0-9]*.tm"));
if (children.data == NULL) print_err("Could not read the installed package directory: ", installed);
if (cross_compiling) {
// Installed package archives were compiled for the native
// platform, so cross builds recompile the package's modules from
// their installed sources (into per-target .tomo directories)
// and link those objects instead of package.a:
for (int64_t i = 0; i < (int64_t)children.length; i++) {
Path_t *child = (Path_t *)(children.data + i * children.stride);
build_file_dependency_graph(build_info, *child, to_compile, to_link);
}
break;
}
Text_t lib = Texts(installed, "/package.a");
Table$set(to_link, &lib, NULL, Table$info(&Text$info, &Void$info));
for (int64_t i = 0; i < (int64_t)children.length; i++) {
Path_t *child = (Path_t *)(children.data + i * children.stride);
Table_t discarded = {.entries = EMPTY_LIST, .fallback = to_compile};
build_file_dependency_graph(build_info, *child, &discarded, to_link);
}
break;
}
case USE_SHARED_OBJECT: {
Text_t lib = Text$from_str(use->path);
Table$set(to_link, &lib, NULL, Table$info(&Text$info, &Void$info));
break;
}
case USE_ASM: {
Path_t asm_path = Path$from_str(use->path);
OptionalPath_t source_dir = Path$parent(path);
assert(source_dir); // A source file always has a directory
asm_path = Path$concat(source_dir, asm_path);
Text_t linker_text = Path$as_text(&asm_path, NULL, &Path$info);
Table$set(to_link, &linker_text, NULL, Table$info(&Text$info, &Void$info));
if (is_stale(build_file(path, ".o"), asm_path, false)) {
staleness.o = true;
Table$set(to_compile, &path, &staleness, Table$info(&Path$info, &Byte$info));
}
break;
}
case USE_HEADER:
case USE_C_CODE: {
if (use->path[0] == '<') break;
OptionalPath_t source_dir = Path$parent(path);
assert(source_dir); // A source file always has a directory
Path_t dep_path = Path$resolved(Path$from_str(use->path), source_dir);
if (is_stale(build_file(path, ".o"), dep_path, false)) {
staleness.o = true;
Table$set(to_compile, &path, &staleness, Table$info(&Path$info, &Byte$info));
}
break;
}
default: break;
}
}
}
static Table_t c_modification_times = EMPTY_TABLE;
static const TypeInfo_t include_time_info = {.size = sizeof(time_t), .align = __alignof__(time_t), .tag = OpaqueInfo};
// The file being scanned, and the newest modification time seen for it or
// anything it includes so far:
typedef struct {
Path_t path;
time_t latest;
} include_scan_t;
time_t latest_included_modification_time(Path_t path);
static void note_included_modification_time(void *userdata, Path_t included) {
include_scan_t *scan = (include_scan_t *)userdata;
time_t included_time = latest_included_modification_time(included);
if (included_time > scan->latest) {
scan->latest = included_time;
Table$set(&c_modification_times, &scan->path, &scan->latest, Table$info(&Path$info, &include_time_info));
}
}
time_t latest_included_modification_time(Path_t path) {
time_t *cached_latest = Table$get(c_modification_times, &path, Table$info(&Path$info, &include_time_info));
if (cached_latest) return *cached_latest;
struct stat s;
include_scan_t scan = {.path = path, .latest = 0};
if (stat(Path$as_c_string(path), &s) == 0) scan.latest = s.st_mtime;
// Cached before the scan below, so an include cycle terminates:
Table$set(&c_modification_times, &path, &scan.latest, Table$info(&Path$info, &include_time_info));
scan_quoted_includes(path, note_included_modification_time, &scan);
return scan.latest;
}
bool is_stale(Path_t path, Path_t relative_to, bool ignore_missing) {
struct stat target_stat;
if (stat(Path$as_c_string(path), &target_stat) != 0) {
if (ignore_missing) return false;
return true;
}
#ifdef __linux__
// Any file older than the compiler is stale:
if (target_stat.st_mtime < compiler_stat.st_mtime) return true;
#endif
if (Path$has_extension(relative_to, Text("c")) || Path$has_extension(relative_to, Text("h"))
|| Path$has_extension(relative_to, Text("s")) || Path$has_extension(relative_to, Text("S"))) {
time_t mtime = latest_included_modification_time(relative_to);
return target_stat.st_mtime < mtime;
}
struct stat relative_to_stat;
if (stat(Path$as_c_string(relative_to), &relative_to_stat) != 0) {
if (ignore_missing) return false;
print_err("File doesn't exist: ", relative_to);
}
return target_stat.st_mtime < relative_to_stat.st_mtime;
}
bool is_stale_for_any(Path_t path, List_t relative_to, bool ignore_missing) {
for (int64_t i = 0; i < (int64_t)relative_to.length; i++) {
Path_t r = *(Path_t *)(relative_to.data + i * relative_to.stride);
if (is_stale(path, r, ignore_missing)) return true;
}
return false;
}
void transpile_header(env_t *base_env, Path_t path) {
Path_t h_filename = build_file(path, ".h");
ast_t *ast = parse_file(Path$as_c_string(path), NULL);
if (!ast) print_err("Could not parse file: ", path);
env_t *module_env = load_module_env(base_env, ast);
Text_t h_code = compile_file_header(module_env, Path$resolved(h_filename, Path$from_str(".")), ast);
Path_t h_temp = artifact_temp(h_filename);
FILE *header = fopen(Path$as_c_string(h_temp), "w");
if (!header) print_err("Failed to open header file: ", h_temp);
Text$print(header, h_code);
if (fclose(header) == -1) print_err("Failed to write header file: ", h_temp);
commit_artifact(h_temp, h_filename);
LOG(LOG_BUILD, "Transpiled header:\t", Path$relative_to(h_filename, Path$current_dir()));
}
void transpile_code(env_t *base_env, Path_t path) {
Path_t c_filename = build_file(path, ".c");
ast_t *ast = parse_file(Path$as_c_string(path), NULL);
if (!ast) print_err("Could not parse file: ", path);
env_t *module_env = load_module_env(base_env, ast);
Text_t c_code = compile_file(module_env, ast);
binding_t *main_binding = get_binding(module_env, "main");
bool has_main = main_binding && main_binding->type->tag == FunctionType;
cli_command_def_t *subcommands = get_cli_subcommands(module_env);
if (has_main || subcommands) {
Text_t entry = namespace_name(module_env, module_env->namespace, Text("main"));
Text_t dispatch = compile_cli_dispatch(module_env, ast, subcommands, entry);
// The generated command-line wrapper is code the program never wrote,
// so it must not inherit the .tm line numbering compile_file() opens
// the file with. Left alone, every line of the wrapper claims to be a
// line of the .tm file, counting on from that opening `#line 1`,
// and a debugger stopped in it reports whichever Tomo code happens to
// sit at those line numbers. Point it back at the C file it actually
// is, which means knowing how many lines come before it.
if (module_env->do_source_mapping) {
int64_t line = 1;
char last = '\n';
for (const char *p = Text$as_c_string(c_code); *p; p++) {
if (*p == '\n') line += 1;
last = *p;
}
// A `#line` is a preprocessor directive, so it has to start its own
// line. If the code so far didn't end with a newline, break the
// line first (which puts the directive one line further down).
Text_t newline = EMPTY_TEXT;
if (last != '\n') {
newline = Text("\n");
line += 1;
}
// `line` is now the directive's own line, and a `#line` numbers the
// line after itself:
dispatch =
Texts(newline, "#line ", line + 1, " ", quoted_str(Path$as_c_string(c_filename)), "\n", dispatch);
}
c_code = Texts(c_code, dispatch);
}
Path_t c_temp = artifact_temp(c_filename);
FILE *c_file = fopen(Path$as_c_string(c_temp), "w");
if (!c_file) print_err("Failed to write C file: ", c_temp);
Text$print(c_file, c_code);
if (fclose(c_file) == -1) print_err("Failed to output C code to ", c_temp);
commit_artifact(c_temp, c_filename);
LOG(LOG_BUILD, "Transpiled code:\t", Path$relative_to(c_filename, Path$current_dir()));
}
// The first time the bundled Zig toolchain compiles anything on a machine, it
// builds its libc (musl, compiler-rt, etc.) from source and stores it in its
// global cache; that one-time setup takes tens of seconds and can look like a
// hang. Detect a missing cache and print a notice so users know what's
// happening. ZIG_GLOBAL_CACHE_DIR is always set: main() points it into Tomo's
// own cache directory unless the environment already had it.
static void warn_if_first_compile(void) {
static bool already_checked = false;
if (already_checked) return;
already_checked = true;
const char *dir = getenv("ZIG_GLOBAL_CACHE_DIR");
if (dir == NULL || dir[0] == '\0') return;
struct stat st;
if (stat(dir, &st) != 0) {
fprint(stderr, USE_COLOR ? "\x1b[93;1m" : "",
"First compile on this machine: the bundled Zig toolchain is building its libc cache "
"(a one-time setup that takes a little while)...",
USE_COLOR ? "\x1b[m" : "");
fflush(stderr);
}
}
void compile_object_file(Path_t path) {
warn_if_first_compile();
Path_t obj_file = build_file(path, ".o");
Path_t c_file = build_file(path, ".c");
Path_t obj_temp = artifact_temp(obj_file);
if (!run_compile(String("-c ", c_file, " -o ", obj_temp), obj_temp)) {
unlink(Path$as_c_string(obj_temp)); // don't leave a partial object looking like a finished one
exit(EXIT_FAILURE);
}
commit_artifact(obj_temp, obj_file);
// After the object, so a reader that sees a fresh .config always finds a finished .o behind it:
Path_t config_file = build_file(path, ".config");
Path_t config_temp = artifact_temp(config_file);
Path$write(config_temp, config_summary, 0644);
commit_artifact(config_temp, config_file);
LOG(LOG_BUILD, "Compiled object:\t", Path$relative_to(obj_file, Path$current_dir()));
}
// Manpages document the code that is defined in source code files, so editing
// the source code requires rebuilding the manpage.
static bool manpage_is_stale(Path_t manpage_file, cli_command_def_t *commands) {
for (cli_command_def_t *cmd = commands; cmd; cmd = cmd->next) {
if (cmd->binding && is_stale(manpage_file, Path$from_str(cmd->binding->fn_def->file->filename), true))
return true;
if (manpage_is_stale(manpage_file, cmd->children)) return true;
}
return false;
}
Path_t compile_executable(env_t *base_env, Path_t path, Path_t exe_path, List_t object_files, List_t extra_ldlibs,
bool embed_git_info, bool embed_source) {
warn_if_first_compile();
ast_t *ast;
TOMO_PROFILE_SPAN("parse (main)", ast = parse_file(Path$as_c_string(path), NULL));
if (!ast) print_err("Could not parse file ", path);
env_t *env;
TOMO_PROFILE_SPAN("load module env", env = load_module_env(base_env, ast));
binding_t *main_binding = get_binding(env, "main");
cli_command_def_t *subcommands = get_cli_subcommands(env);
if ((main_binding && main_binding->type->tag == FunctionType) || subcommands) {
Path_t manpage_file = build_file(Path$with_extension(path, Text(".1"), true), "");
if (clean_build || !Path$is_file(manpage_file, true) || is_stale(manpage_file, path, true)
|| manpage_is_stale(manpage_file, subcommands)) {
Text_t manpage = compile_manpage(Path$base_name(exe_path), ast,
main_binding && main_binding->type->tag == FunctionType
? Match(main_binding->type, FunctionType)->args
: NULL,
subcommands);
Path$write(manpage_file, manpage, 0644);
LOG(LOG_BUILD, "Wrote manpage:\t", Path$relative_to(manpage_file, Path$current_dir()));
} else {
LOG(LOG_SKIP, "Unchanged: ", manpage_file);
}
}
// Rebuilding is also needed if any linked package archive (like an
// edited vendored package's recompiled package.a) is newer than the
// executable:
List_t linked_archives = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)extra_ldlibs.length; i++) {
Text_t *lib = (Text_t *)(extra_ldlibs.data + i * extra_ldlibs.stride);
if (!Text$ends_with(*lib, Text(".a"), NULL)) continue;
Path_t archive_path = Path$from_text(*lib);
List$insert(&linked_archives, &archive_path, I(0), sizeof(Path_t));
}
OptionalPath_t exe_packages_ini = Path$sibling(path, Text("packages.ini"));
assert(exe_packages_ini); // A source file always has a directory
if (!clean_build && Path$is_file(exe_path, true) && !is_config_outdated(path)
&& !is_stale_for_any(exe_path, object_files, false) && !is_stale_for_any(exe_path, linked_archives, true)
&& !is_stale(exe_path, exe_packages_ini, true)) {
LOG(LOG_SKIP, "Unchanged: ", exe_path);
return exe_path;
}
// Git provenance (commit, dirty flag) is embedded in the binary's
// build_info. It costs a few git subprocesses, so skip it for ephemeral
// `tomo run`/`eval` executables, where the metadata is thrown away with the
// binary; only persistent `build`/`install` artifacts pay for it:
OptionalPath_t source_dir = Path$parent(path);
assert(source_dir); // A source file always has a directory
if (embed_git_info) TOMO_PROFILE_SPAN("git info", add_git_info(env, source_dir));
// Zip up the program's sources for embedding into the executable
// (`--skip-source` leaves them out, so the binary can't be unpacked back
// into a buildable project):
Path_t source_blob = build_file(path, ".source.zip");
if (embed_source) TOMO_PROFILE_SPAN("source blob", write_source_blob(env, path, exe_path, source_blob));
// An `--instrument` binary starts profiling before anything else runs and
// prints its report at exit (PROFILE=0 opts out, PROFILE_FILE redirects).
// Its command-line arguments are left entirely alone. See
// tomo_profile_start() in src/stdlib/profiling.c.
Text_t profiler_decl = instrument ? Text("extern void tomo_profile_start(void);\n") : EMPTY_TEXT;
Text_t start_profiler = instrument ? Text("\ttomo_profile_start();\n") : EMPTY_TEXT;
Text_t program;
if ((main_binding && main_binding->type->tag == FunctionType) || subcommands) {
Text_t entry = main_binding && main_binding->type->tag == FunctionType
? main_binding->code
: namespace_name(env, env->namespace, Text("main"));
program = Texts("extern int parse_and_run$$", entry, "(int argc, char *argv[]);\n", profiler_decl,
"__attribute__ ((noinline))\n"
"int main(int argc, char *argv[]) {\n",
start_profiler, "\treturn parse_and_run$$", entry,
"(argc, argv);\n"
"}\n",
compile_build_info(env, "build_info"),
(link_macho || !embed_source) ? EMPTY_TEXT : compile_source_asm(source_blob));
} else {
program = Texts("extern void ", namespace_name(env, env->namespace, Text("$initialize")),
"(void);\n"
"extern void tomo_init(void);\n",
profiler_decl,
"__attribute__ ((noinline))\n"
"int main(int argc, char *argv[]) {\n",
start_profiler, "tomo_init();\n", namespace_name(env, env->namespace, Text("$initialize")),
"();\n"
"\n",
"return 0;\n"
"}\n",
compile_build_info(env, "build_info"),
(link_macho || !embed_source) ? EMPTY_TEXT : compile_source_asm(source_blob));
}
Path_t runner_file = build_file(path, ".runner.c");
Path$write(runner_file, program, 0644);
// Libraries bundled with the Tomo toolchain: every program links the full
// vendored archives (below), so a package's `use -lgmp` etc. must not
// become a -l flag, since no system copies exist (the toolchain uses its
// own static musl builds):
static const char *bundled_libs[] = {"-lgc", "-lgmp", "-lunistring", "-lbacktrace", "-lm", "-lunwind"};
// .a archive files need to go later in the positional order:
List_t archives = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)extra_ldlibs.length;) {
Text_t *lib = (Text_t *)(extra_ldlibs.data + i * extra_ldlibs.stride);
bool bundled = false;
for (size_t j = 0; j < sizeof(bundled_libs) / sizeof(bundled_libs[0]); j++)
bundled = bundled || Text$equal_values(*lib, Text$from_str(bundled_libs[j]));
if (bundled) {
List$remove_at(&extra_ldlibs, I(i + 1), I(1), sizeof(Text_t));
} else if (Text$ends_with(*lib, Text(".a"), NULL)) {
List$insert(&archives, lib, I(0), sizeof(Text_t));
List$remove_at(&extra_ldlibs, I(i + 1), I(1), sizeof(Text_t));
} else {
i += 1;
}
}
// The vendored static libraries that libtomo (and any package using their
// headers) is linked against:
Text_t vendor_dir = Texts(lib_root, "/lib/tomo@", TOMO_VERSION, "/vendor");
Text_t vendor_archives = Texts(" ", vendor_dir, "/libgc.a ", vendor_dir, "/libgmp.a ", vendor_dir,
"/libunistring.a ", vendor_dir, "/libbacktrace.a");
// The debug-stripped zig C runtime replacing the libc that -nostdlib told
// zig not to add (see tomo_configure()). Last, like an implicit libc; the
// set of archives varies by zig version, so link whichever were staged:
if (zig_libc_dir.length > 0) {
static const char *runtime_archives[] = {"libc.a", "libcompiler_rt.a", "libzigc.a", "libunwind.a"};
for (size_t i = 0; i < sizeof(runtime_archives) / sizeof(runtime_archives[0]); i++) {
const char *archive = String(zig_libc_dir, "/", runtime_archives[i]);
if (Path$is_file(Path$from_str(archive), true))
vendor_archives = Texts(vendor_archives, Text(" '"), Text$from_str(archive), Text("'"));
}
}
// On Mach-O the source blob is embedded by the linker rather than asm:
Text_t source_section_flag = (link_macho && embed_source) ? Texts(" '-Wl,-sectcreate,__TEXT,__tomo_source,",
Text$from_str(Path$as_c_string(source_blob)), "'")
: EMPTY_TEXT;
TOMO_PROFILE_SPAN_BEGIN(link_span, "cc link executable");
FILE *runner = run_cmd( // Invoke C compiler
cc,
// C flags:
" ", cflags, " -O", optimization,
// Linker flags and dynamically linked shared packages (link_optimizations
// holds the size-reducing flags that optimized builds add, empty for the
// fast run/eval path):
" ", ldflags, link_optimizations, source_section_flag, " ", ldlibs, " ", list_text(extra_ldlibs),
// Object files:
" ", paths_str(object_files),
// Input file:
" ", runner_file,
// Statically linked archive files (must come after runner). No archive
// grouping is needed for circular dependencies among packages: zig links
// with lld, which resolves archive members iteratively.
" ", list_text(archives),
// Tomo static library (Mach-O linking has no --no-whole-archive):
link_macho ? "" : " -Wl,--no-whole-archive", " ", lib_root, "/lib/tomo@", TOMO_VERSION, "/libtomo.a",
vendor_archives,
// Output file:
" -o ", exe_path);
Text$print(runner, program);
int status = pclose(runner);
TOMO_PROFILE_SPAN_END(link_span);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) exit(EXIT_FAILURE);
LOG(LOG_BUILD, "Compiled executable:\t", Path$relative_to(exe_path, Path$current_dir()));
OptionalPath_t package_dir = Path$parent(path);
assert(package_dir); // A source file always has a directory
gc_package_dir(package_dir);
return exe_path;
}
Path_t build_test_runner(Path_t path, List_t object_files, List_t extra_ldlibs, Text_t test_source) {
Path_t test_c = build_file(path, ".test.c");
Path$write(test_c, test_source, 0644);
Path_t test_o = build_file(path, ".test.o");
Path_t exe_path = build_file(path, ".test-runner");
// Compile the runner to an object with clean cflags first: the link line
// below carries -nostdlib and other ldflags that break system-header search
// (<math.h> etc.) if the .c were compiled inline like compile_executable's
// trivial runner.c (which doesn't include <tomo.h>).
if (!run_compile(String("-c ", test_c, " -o ", test_o), test_o)) exit(EXIT_FAILURE);
// The runner TU already contains the module's full code (so tests can reach
// its private helpers), so drop the module's own object file from the link
// to avoid duplicate definitions of its public symbols. Compare resolved
// absolute paths, since the dependency-graph objects and build_file(path)
// can carry different (relative vs absolute) representations of the same
// file:
Path_t cwd = Path$current_dir();
const char *module_obj = Path$as_c_string(Path$resolved(build_file(path, ".o"), cwd));
List_t link_objects = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)object_files.length; i++) {
Path_t *obj = (Path_t *)(object_files.data + i * object_files.stride);
if (!streq(Path$as_c_string(Path$resolved(*obj, cwd)), module_obj))
List$insert(&link_objects, obj, I(0), sizeof(Path_t));
}
object_files = link_objects;
// Same archive juggling as compile_executable: drop the always-bundled libs
// and push .a archives after the object files in link order.
static const char *bundled_libs[] = {"-lgc", "-lgmp", "-lunistring", "-lbacktrace", "-lm", "-lunwind"};
List_t archives = EMPTY_LIST;
for (int64_t i = 0; i < (int64_t)extra_ldlibs.length;) {
Text_t *lib = (Text_t *)(extra_ldlibs.data + i * extra_ldlibs.stride);
bool bundled = false;
for (size_t j = 0; j < sizeof(bundled_libs) / sizeof(bundled_libs[0]); j++)
bundled = bundled || Text$equal_values(*lib, Text$from_str(bundled_libs[j]));
if (bundled) {
List$remove_at(&extra_ldlibs, I(i + 1), I(1), sizeof(Text_t));
} else if (Text$ends_with(*lib, Text(".a"), NULL)) {
List$insert(&archives, lib, I(0), sizeof(Text_t));
List$remove_at(&extra_ldlibs, I(i + 1), I(1), sizeof(Text_t));
} else {
i += 1;
}
}
Text_t vendor_dir = Texts(lib_root, "/lib/tomo@", TOMO_VERSION, "/vendor");
Text_t vendor_archives = Texts(" ", vendor_dir, "/libgc.a ", vendor_dir, "/libgmp.a ", vendor_dir,
"/libunistring.a ", vendor_dir, "/libbacktrace.a");
if (zig_libc_dir.length > 0) {
static const char *runtime_archives[] = {"libc.a", "libcompiler_rt.a", "libzigc.a", "libunwind.a"};
for (size_t i = 0; i < sizeof(runtime_archives) / sizeof(runtime_archives[0]); i++) {
const char *archive = String(zig_libc_dir, "/", runtime_archives[i]);
if (Path$is_file(Path$from_str(archive), true))
vendor_archives = Texts(vendor_archives, Text(" '"), Text$from_str(archive), Text("'"));
}
}
FILE *runner = run_cmd(cc, " ", cflags, " -O", optimization, " ", ldflags, link_optimizations, " ", ldlibs, " ",
list_text(extra_ldlibs), " ", paths_str(object_files), " ", test_o, " ", list_text(archives),
link_macho ? "" : " -Wl,--no-whole-archive", " ", lib_root, "/lib/tomo@", TOMO_VERSION,
"/libtomo.a", vendor_archives, " -o ", exe_path);
int status = pclose(runner);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) exit(EXIT_FAILURE);
LOG(LOG_BUILD, "Compiled test runner:\t", Path$relative_to(exe_path, Path$current_dir()));
return exe_path;
}
|