mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
Windows: the compiler made from bootstrap/windows-x86_64.ll runs under Wine, compiles itself to the same IR, builds an exe of itself, and every test program passes through it. It needed: Windows-sized unsigned long for libclang's unsaved file, no 'cross' when Windows builds for Windows, --target passed to libclang (libclang.dll assumes MSVC), the MinGW triple always in the IR, FINCH_LDFLAGS, .exe names and FINCH_LLVM_INCLUDE in bootstrap/build.sh, winseed.sh/winbuild.sh. macOS: seeds for arm64 and x86-64 made with the 11.3 SDK; IR is accepted by clang, nothing has run on a Mac yet. CI gets a windows and a macos job. UNTESTED on real Windows/macOS runners. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015f73FesGZGYPkmDvUcj6QX
299 lines
9.5 KiB
Text
299 lines
9.5 KiB
Text
// From LLVM IR to a program: compile the IR, add the runtime (compiled once and kept), link the C libraries
|
|
// and C files the program asks for, and turn the linker's complaints into something a person can act on.
|
|
import platform
|
|
import cimport
|
|
import host
|
|
|
|
// 32-bit FNV-1a, as hex: names the cached runtime object after what it was built from
|
|
fn fingerprint(str text) -> str {
|
|
u32 h = 2166136261
|
|
for c in text {
|
|
h = (h ^ u32(c)) * 16777619
|
|
}
|
|
digits := "0123456789abcdef"
|
|
[]str parts
|
|
for k in 0..8 {
|
|
parts.push(str(digits[int((h >> u32(28 - 4 * k)) & 15)]))
|
|
}
|
|
return parts.join("")
|
|
}
|
|
|
|
fn quote(str a) -> str {
|
|
return host.quote(a)
|
|
}
|
|
|
|
// "-lname", or whatever pkg-config says the library needs
|
|
fn libFlags(str name, platform.Target tg) -> str {
|
|
if name.ends_with(".framework") {
|
|
return "-framework " + quote(name.sub(0, name.len - 10)) // macOS: link "Cocoa.framework"
|
|
}
|
|
if !tg.cross {
|
|
tmp := host.tempDir() + "/finch_pkg_flags_" + str(host.pid()) + ".txt"
|
|
code := host.run("pkg-config --libs " + quote(name) + " > " + quote(tmp) + " 2> " + host.nullDevice())
|
|
if code == 0 && file_exists(tmp) {
|
|
flags := (read_file(tmp) or "").trim()
|
|
host.remove(tmp)
|
|
if flags.len > 0 {
|
|
return flags
|
|
}
|
|
}
|
|
}
|
|
return quote("-l" + name)
|
|
}
|
|
|
|
// "GLFW/glfw3.h" -> "glfw": a decent first guess for the library's name
|
|
fn guessLib(str header) -> str {
|
|
name := header
|
|
slash := name.find("/")
|
|
while slash >= 0 {
|
|
name = name.sub(slash + 1, name.len)
|
|
slash = name.find("/")
|
|
}
|
|
dot := name.len
|
|
for i in 0..name.len {
|
|
if name[i] == '.' {
|
|
dot = i
|
|
}
|
|
}
|
|
name = name.sub(0, dot)
|
|
while name.len > 0 && name[name.len - 1] >= '0' && name[name.len - 1] <= '9' {
|
|
name = name.sub(0, name.len - 1)
|
|
}
|
|
return name.lower()
|
|
}
|
|
|
|
struct Names {
|
|
[]str items
|
|
}
|
|
|
|
fn Names.add(str x) {
|
|
if x.len > 0 && !self.items.contains(x) {
|
|
self.items.push(x)
|
|
}
|
|
}
|
|
|
|
// the text after `marker` up to the next one of `stops`
|
|
fn between(str line, str marker, str stops) -> str {
|
|
a := line.find(marker)
|
|
if a < 0 {
|
|
return ""
|
|
}
|
|
rest := line.sub(a + marker.len, line.len)
|
|
n := 0
|
|
while n < rest.len && !stops.contains(str(rest[n])) {
|
|
n += 1
|
|
}
|
|
return rest.sub(0, n)
|
|
}
|
|
|
|
// The linker speaks in riddles; say what is missing and what to write.
|
|
fn linkFailed(str output, cimport.CImports ci, platform.Target tg) -> int {
|
|
missing := Names()
|
|
libs := Names()
|
|
for line in output.split("\n") {
|
|
l := line.trim()
|
|
if l.contains("undefined reference to `") {
|
|
missing.add(between(l, "undefined reference to `", "'"))
|
|
} else if l.contains("undefined symbol: ") {
|
|
missing.add(between(l, "undefined symbol: ", " \n"))
|
|
} else if l.contains("unresolved external symbol ") {
|
|
missing.add(between(l, "unresolved external symbol ", " \n\r"))
|
|
} else if l.contains("cannot find -l") {
|
|
libs.add(between(l, "cannot find -l", ": \n\r"))
|
|
} else if l.contains("library not found for -l") {
|
|
libs.add(between(l, "library not found for -l", ": \n\r"))
|
|
} else if l.contains("ld: library '") {
|
|
libs.add(between(l, "ld: library '", "'"))
|
|
} else if l.contains("framework '") && l.contains("not found") {
|
|
libs.add(between(l, "framework '", "'") + ".framework")
|
|
} else if l.starts_with("\"") && l.contains("\", referenced from:") {
|
|
// Apple ld: "_name", referenced from:
|
|
name := l.sub(1, l.find("\", referenced from:"))
|
|
if tg.darwin && name.starts_with("_") {
|
|
name = name.sub(1, name.len)
|
|
}
|
|
missing.add(name)
|
|
}
|
|
}
|
|
if missing.items.len == 0 && libs.items.len == 0 {
|
|
print(output + "error: linking failed")
|
|
return 1
|
|
}
|
|
for lib in libs.items {
|
|
print("error: the library '" + lib + "' wasn't found. Is it installed? (on Nix: add it to shell.nix)")
|
|
}
|
|
// the missing functions, grouped by the header they came from
|
|
[]str headers
|
|
[]Names byHeader
|
|
for name in missing.items {
|
|
header := ""
|
|
if ci.fns.has(name) {
|
|
header = ci.fns[name].header
|
|
}
|
|
k := headers.find(header)
|
|
if k < 0 {
|
|
headers.push(header)
|
|
byHeader.push(Names())
|
|
k = headers.len - 1
|
|
}
|
|
byHeader[k].add(name)
|
|
}
|
|
for k in 0..headers.len {
|
|
names := byHeader[k].items
|
|
[]str shown
|
|
for i in 0..names.len {
|
|
if i < 3 {
|
|
shown.push(names[i])
|
|
}
|
|
}
|
|
list := shown.join(", ")
|
|
if names.len > 3 {
|
|
list = list + " and " + str(names.len - 3) + " more"
|
|
}
|
|
if headers[k].len == 0 {
|
|
print("error: no code found for " + list)
|
|
continue
|
|
}
|
|
verb := "come"
|
|
if names.len == 1 {
|
|
verb = "comes"
|
|
}
|
|
print("error: " + list + " " + verb + " from \"" + headers[k] + "\", but its library isn't linked")
|
|
print(" the header only says the functions exist; their code lives in a library.")
|
|
print(" add this at the top of your file (with the library's real name):")
|
|
print("")
|
|
print(" link \"" + guessLib(headers[k]) + "\"")
|
|
print("")
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// The runtime, compiled once per system and compiler and kept in the cache folder.
|
|
fn runtimeObject(str rt, platform.Target tg, str cc) -> str! {
|
|
source := try read_file(rt)
|
|
dir := host.cacheDir()
|
|
obj := dir + "/rt-" + fingerprint(source + cc + tg.triple + "2.5") + ".o"
|
|
if file_exists(obj) {
|
|
return obj
|
|
}
|
|
host.makeDirs(dir)
|
|
part := obj + ".part" + str(host.pid())
|
|
pic := " -fPIC"
|
|
if tg.windows {
|
|
pic = ""
|
|
}
|
|
log := part + ".log"
|
|
code := host.run(cc + " -O2" + pic + " -c " + quote(rt) + " -o " + quote(part) + " > " + quote(log) + " 2>&1")
|
|
if code != 0 {
|
|
text := read_file(log) or ""
|
|
host.remove(part)
|
|
host.remove(log)
|
|
return error(text + "error: couldn't build the Finch runtime with '" + cc + "' (is a C compiler installed?)")
|
|
}
|
|
host.remove(log)
|
|
host.move(part, obj)
|
|
return obj
|
|
}
|
|
|
|
struct Build {
|
|
str out // the program
|
|
str ir // its LLVM IR, as text
|
|
str rt // the runtime's source file
|
|
str opt // -O2 or -O0
|
|
str extra // -g, -fPIC ...
|
|
bool shared
|
|
[]str links // what the program asked to link: a library name, a .c file (with its folder), a .o or .a
|
|
}
|
|
|
|
// IR -> program. 0 if it worked.
|
|
fn build(Build b, platform.Target tg, cimport.CImports ci) -> int {
|
|
clang := getenv("FINCH_CLANG")
|
|
if clang.len == 0 {
|
|
clang = "clang"
|
|
}
|
|
// the C compiler of the runtime and the linker: the target's own when building for another system
|
|
cc := clang
|
|
if tg.cross {
|
|
cc = tg.cc
|
|
} else if getenv("FINCH_CC").len > 0 {
|
|
cc = getenv("FINCH_CC")
|
|
}
|
|
rtobj := runtimeObject(b.rt, tg, cc) or {
|
|
print(err)
|
|
return 1
|
|
}
|
|
ll := b.out + ".ll"
|
|
obj := b.out + ".o"
|
|
write_file(ll, b.ir)
|
|
target := ""
|
|
if tg.cross || tg.windows {
|
|
target = " --target=" + tg.triple
|
|
}
|
|
log := b.out + ".log"
|
|
code := host.run(clang + target + " " + b.opt + b.extra + " -w -c " + quote(ll) + " -o " + quote(obj) + " > " + quote(log) + " 2>&1")
|
|
host.remove(ll)
|
|
if code != 0 {
|
|
print(read_file(log) or "")
|
|
print("error: clang couldn't compile the program's IR")
|
|
host.remove(obj)
|
|
host.remove(log)
|
|
return 1
|
|
}
|
|
cmd := cc + " " + quote(obj) + " " + quote(rtobj) + " -o " + quote(b.out)
|
|
if !tg.msvc {
|
|
cmd = cmd + " -lm"
|
|
}
|
|
if getenv("FINCH_LDFLAGS").len > 0 {
|
|
cmd = cmd + " " + getenv("FINCH_LDFLAGS") // more flags for the linker, like -L/where/libclang/is
|
|
}
|
|
if b.shared {
|
|
if tg.darwin {
|
|
cmd = cmd + " -dynamiclib"
|
|
} else {
|
|
cmd = cmd + " -shared"
|
|
}
|
|
}
|
|
[]str temps
|
|
for l in b.links {
|
|
if l.ends_with(".c") {
|
|
// the program's own C file, built along with it
|
|
o := b.out + "." + str(temps.len) + ".o"
|
|
pic := " -fPIC"
|
|
if tg.windows {
|
|
pic = ""
|
|
}
|
|
c := host.run(cc + " -O2" + pic + " -c " + quote(l) + " -o " + quote(o) + " > " + quote(log) + " 2>&1")
|
|
if c != 0 {
|
|
print((read_file(log) or "") + "error: couldn't compile " + l)
|
|
host.remove(obj)
|
|
host.remove(log)
|
|
host.remove(o)
|
|
return 1
|
|
}
|
|
temps.push(o)
|
|
cmd = cmd + " " + quote(o)
|
|
} else if l.ends_with(".o") || l.ends_with(".a") {
|
|
cmd = cmd + " " + quote(l)
|
|
} else {
|
|
cmd = cmd + " " + libFlags(l, tg)
|
|
}
|
|
}
|
|
status := host.run(cmd + " > " + quote(log) + " 2>&1")
|
|
output := read_file(log) or ""
|
|
host.remove(obj)
|
|
host.remove(log)
|
|
for t in temps {
|
|
host.remove(t)
|
|
}
|
|
if status == 127 {
|
|
print("error: no C compiler found to link with ('" + cc + "'; install one, or set FINCH_CC)")
|
|
return 1
|
|
}
|
|
if status != 0 {
|
|
return linkFailed(output, ci, tg)
|
|
}
|
|
if output.len > 0 {
|
|
print(output) // warnings, if any
|
|
}
|
|
return 0
|
|
}
|