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