main
buildc.rv
1const engine = import "./build.rv"
2
3const settings = {
4 # we use zig's C toolchain by default.
5 cc = {"zig", "cc"},
6 ar = {"zig", "ar"},
7 flags = {},
8 link_flags = {},
9 include_dirs = {},
10 defines = {},
11 deps = {},
12}
13
14type Flags = table<string>
15 type Compilation = {file: string, output: string, command: Flags}
16
17#* kept for compile_commands.json *#
18const compilations: table<Compilation> = {}
19
20#* override the global c settings *#
21pub fn configure(opts: table) -> :nil do
22 for key in table.keys(opts) do
23 if not table.key?(settings, key) do
24 panic("unknown C setting:", key)
25 end
26
27 settings[key] = opts[key]
28 end
29
30 return :nil
31end
32
33fn combined(opts: table, key: atom) -> Flags do
34 return settings[key]:merge(table.key(opts, key) orelse {})
35end
36
37#*
38 run a command into a temp path, then move it into place
39 the returned command may panic, sorry
40*#
41fn action(path: string, args: Flags) -> (fn() -> :ok) do
42 return fn() do
43 const parts = path:split("/")
44 parts:pop()
45 const dir = table.join(parts, "/")
46
47 if dir != "" do
48 fs.mkdir(dir, :true):unwrap()
49 end
50
51 const temporary = path ~ ".tmp"
52
53 if fs.exists?(temporary) do
54 fs.remove(temporary):unwrap()
55 end
56
57 engine.cmd(args)
58
59 return fs.rename(temporary, path):unwrap()
60 end
61end
62
63fn output_rule(kind: atom, path: string, opts: table) -> string do
64 const cc = table.key(opts, :cc) orelse settings.cc
65 const deps = combined(opts, :deps)
66
67 # editing the manifest invalidates its outputs too.
68 deps:push(argv[0])
69
70 const sources = table.key(opts, :sources) orelse {}
71 const objects = table.copy(table.key(opts, :objects) orelse {})
72
73 for source in sources do
74 const stem =
75 if (source:ends_with?(".c"))
76 source:sub(0, source:len() - 2)
77 else
78 source
79
80 const object_name = stem:split("/"):last()
81 const object = path ~ ".obj/" ~ object_name ~ ".o"
82 const args = cc:merge(combined(opts, :flags))
83
84 for dir in combined(opts, :include_dirs) do
85 args:push("-I", dir)
86 end
87
88 for define in combined(opts, :defines) do
89 args:push("-D" ~ define)
90 end
91
92 if kind == :shared do
93 args:push("-fPIC")
94 end
95
96 const temporary = object ~ ".tmp"
97 args:push("-c", source, "-o", temporary)
98
99 # push the real one not temp
100 const command = table.copy(args)
101 command[table.len(command) - 1] = object
102 compilations:push({file = source, output = object, command = command})
103
104 const inputs = deps:merge({source})
105 engine.rule(object, inputs, action(object, args))
106 engine.track(object, args)
107
108 const label = "cc " ~ source
109 engine.describe(object, label)
110 objects:push(object)
111 end
112
113 const libraries = table.key(opts, :libraries) orelse {}
114 const inputs = deps
115 :merge(objects)
116 :merge(libraries)
117
118 let args = cc
119
120 if kind == :static do
121 if not table.empty?(libraries) do
122 panic("static libraries take objects, not libraries:", path)
123 end
124
125 const temporary = path ~ ".tmp"
126
127 args = (table.key(opts, :ar) orelse settings.ar):merge({"rcs", temporary})
128 args = args:merge(objects)
129 end else do
130 args = args:merge(combined(opts, :flags))
131
132 if kind == :shared do
133 args:push("-shared")
134 end
135
136 args = args
137 :merge(objects)
138 :merge(libraries)
139 :merge(combined(opts, :link_flags))
140
141 const temporary = path ~ ".tmp"
142 args:push("-o", temporary)
143 end
144
145 engine.rule(path, inputs, action(path, args))
146 engine.track(path, args)
147
148 const prefixes = {executable = "ld ", static = "ar ", shared = "shared "}
149 const link_label = prefixes[kind] ~ path
150 engine.describe(path, link_label)
151
152 return path
153end
154
155#* build an executable *#
156pub fn executable(path: string, opts: table) -> string
157 return output_rule(:executable, engine.output(path), opts)
158
159#* build an static library *#
160pub fn static_library(path: string, opts: table) -> string
161 return output_rule(:static, engine.output(path), opts)
162
163#* build an shared library *#
164pub fn shared_library(path: string, opts: table) -> string
165 return output_rule(:shared, engine.output(path), opts)
166
167fn quote(arg: string) -> string do
168 if arg:len() > 0
169 and not arg:contains?(" ")
170 and not arg:contains?("\t")
171 and not arg:contains?("'")
172 and not arg:contains?("\"") do
173 return arg
174 end
175
176 return "'" ~ arg:replace("'", "'\\''") ~ "'"
177end
178
179#*
180 write compile_commands.json into pwd for every source compiled so far
181 just put this at the end of your script
182*#
183pub fn compile_commands() -> :nil do
184 const directory = cwd()
185
186 const entries =
187 to_iter(compilations)
188 :map(fn(compilation) do
189 const quoted =
190 to_iter(compilation.command)
191 :map(quote)
192 :collect()
193
194 return {
195 directory = directory,
196 file = compilation.file,
197 output = compilation.output,
198 command = quoted:join(" "),
199 }
200 end)
201 :collect()
202
203 const text = json.encode(entries, :true)?
204 const file = fs.open("compile_commands.json", "w")?
205
206 file:write(text ~ "\n")?
207 file:close()?
208
209 return :nil
210end
211
212pub fn build(outputs: table<string>) -> :ok do
213 return engine.run(outputs)
214end