Skip to content

Commit 78455fe

Browse files
committed
Experimental support for bulding system libraries via ninja
The primary advantage of doing this is that it avoids having force a completely rebuild of a system library when you want to rebuilt it. Instead not have precise dependencies.
1 parent 9194ea9 commit 78455fe

File tree

4 files changed

+157
-39
lines changed

4 files changed

+157
-39
lines changed

embuilder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def main():
237237
if do_clear:
238238
library.erase()
239239
if do_build:
240-
library.get_path()
240+
library.build()
241241
elif what == 'sysroot':
242242
if do_clear:
243243
shared.Cache.erase_file('sysroot_install.stamp')

tools/cache.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ def get_lib_dir(self, absolute, varies=True):
108108
path = Path(path, '-'.join(subdir))
109109
return path
110110

111-
def get_lib_name(self, name, varies=True):
112-
return str(self.get_lib_dir(absolute=False, varies=varies).joinpath(name))
111+
def get_lib_name(self, name, varies=True, absolute=False):
112+
return str(self.get_lib_dir(absolute=absolute, varies=varies).joinpath(name))
113113

114114
def erase_lib(self, name):
115115
self.erase_file(self.get_lib_name(name))
@@ -127,7 +127,7 @@ def get_lib(self, libname, *args, **kwargs):
127127

128128
# Request a cached file. If it isn't in the cache, it will be created with
129129
# the given creator function
130-
def get(self, shortname, creator, what=None, force=False):
130+
def get(self, shortname, creator, what=None, force=False, quiet=False):
131131
cachename = Path(self.dirname, shortname)
132132
# Check for existence before taking the lock in case we can avoid the
133133
# lock completely.
@@ -147,11 +147,12 @@ def get(self, shortname, creator, what=None, force=False):
147147
what = 'system library'
148148
else:
149149
what = 'system asset'
150-
message = f'generating {what}: {shortname}... (this will be cached in "{cachename}" for subsequent builds)'
150+
message = f'generating {what}: "{cachename}"'
151151
logger.info(message)
152152
utils.safe_ensure_dirs(cachename.parent)
153153
creator(str(cachename))
154154
assert cachename.exists()
155-
logger.info(' - ok')
155+
if not quiet:
156+
logger.info(' - ok')
156157

157158
return str(cachename)

tools/gen_struct_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def inspect_headers(headers, cflags):
246246
# TODO(sbc): If we can remove EM_EXCLUSIVE_CACHE_ACCESS then this would not longer be needed.
247247
shared.check_sanity()
248248

249-
compiler_rt = system_libs.Library.get_usable_variations()['libcompiler_rt'].get_path()
249+
compiler_rt = system_libs.Library.get_usable_variations()['libcompiler_rt'].build()
250250

251251
# Close all unneeded FDs.
252252
os.close(src_file[0])

tools/system_libs.py

Lines changed: 149 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
'getsockopt.c', 'setsockopt.c', 'freeaddrinfo.c',
2929
'in6addr_any.c', 'in6addr_loopback.c', 'accept4.c']
3030

31+
# Experimental: Setting EMCC_USE_NINJA will cause system libraries to get built with ninja rather
32+
# than simple subprocesses. The primary benefit here is that we get accurate dependency tracking.
33+
# This means we can avoid completely rebuilding a library and just rebiuld based on what changed.
34+
#
35+
# Setting EMCC_USE_NINJA=2 means that ninja will automatically be run for each library needed at
36+
# link time.
37+
USE_NINJA = int(os.environ.get('EMCC_USE_NINJA', '0'))
38+
3139

3240
def files_in_path(path, filenames):
3341
srcdir = utils.path_from_root(path)
@@ -239,23 +247,26 @@ def can_build(self):
239247
return True
240248

241249
def erase(self):
242-
shared.Cache.erase_file(shared.Cache.get_lib_name(self.get_filename()))
250+
shared.Cache.erase_file(self.get_path())
251+
252+
def get_path(self, absolute=False):
253+
return shared.Cache.get_lib_name(self.get_filename(), absolute=absolute)
243254

244-
def get_path(self):
255+
def build(self):
245256
"""
246257
Gets the cached path of this library.
247258
248259
This will trigger a build if this library is not in the cache.
249260
"""
250-
return shared.Cache.get_lib(self.get_filename(), self.build)
261+
return shared.Cache.get(self.get_path(), self.do_build, force=USE_NINJA == 2, quiet=USE_NINJA)
251262

252263
def get_link_flag(self):
253264
"""
254265
Gets the link flags needed to use the library.
255266
256267
This will trigger a build if this library is not in the cache.
257268
"""
258-
fullpath = self.get_path()
269+
fullpath = self.build()
259270
# For non-libaries (e.g. crt1.o) we pass the entire path to the linker
260271
if self.get_ext() != '.a':
261272
return fullpath
@@ -281,6 +292,100 @@ def get_files(self):
281292

282293
raise NotImplementedError()
283294

295+
def write_ninja_file(self, filename, libname):
296+
cflags = self.get_cflags()
297+
asflags = get_base_cflags()
298+
# TODO(sbc) There is an llvm bug that causes a crash when `-g` is used with
299+
# assembly files that define wasm globals.
300+
asflags = [arg for arg in asflags if arg != '-g']
301+
cflags_asm = [arg for arg in cflags if arg != '-g']
302+
303+
def join(flags):
304+
return ' '.join(flags)
305+
306+
out = f'''\
307+
# Automatically generated by tools/system_libs.py. DO NOT EDIT
308+
309+
ninja_required_version = 1.5
310+
311+
ASFLAGS = {join(asflags)}
312+
CFLAGS = {join(cflags)}
313+
CFLAGS_ASM = {join(cflags_asm)}
314+
EMCC = {shared.EMCC}
315+
EMXX = {shared.EMXX}
316+
EMAR = {shared.EMAR}
317+
318+
rule cc
319+
depfile = $out.d
320+
command = $EMCC -MD -MF $out.d $CFLAGS -c $in -o $out
321+
description = CC $in
322+
323+
rule cxx
324+
depfile = $out.d
325+
command = $EMXX -MD -MF $out.d $CFLAGS -c $in -o $out
326+
description = CXX $out
327+
328+
rule asm
329+
command = $EMCC $ASFLAGS -c $in -o $out
330+
description = ASM $out
331+
332+
rule asm_cpp
333+
depfile = $out.d
334+
command = $EMCC -MD -MF $out.d $CFLAGS_ASM -c $in -o $out
335+
description = ASM $out
336+
337+
rule direct_cc
338+
depfile = $with_depfile
339+
command = $EMCC -MD -MF $with_depfile $CFLAGS -c $in -o $out
340+
description = CC $out
341+
342+
rule archive
343+
command = $EMAR cr $out $in
344+
description = AR $out
345+
346+
'''
347+
suffix = shared.suffix(libname)
348+
input_files = self.get_files()
349+
350+
if suffix == '.o':
351+
assert len(input_files) == 1
352+
depfile = shared.unsuffixed_basename(input_files[0]) + '.d'
353+
out += f'build {libname}: direct_cc {input_files[0]}\n'
354+
out += f' with_depfile = {depfile}\n'
355+
else:
356+
objects = []
357+
for src in input_files:
358+
o = shared.unsuffixed_basename(src) + '.o'
359+
object_uuid = 0
360+
# Find a unique basename
361+
while o in objects:
362+
object_uuid += 1
363+
o = f'{object_basename}__{object_uuid}.o'
364+
objects.append(o)
365+
ext = shared.suffix(src)
366+
if ext == '.s':
367+
out += f'build {o}: asm {src}\n'
368+
flags = asflags
369+
elif ext == '.S':
370+
out += f'build {o}: asm_cpp {src}\n'
371+
flags = cflags_asm
372+
elif ext == '.c':
373+
out += f'build {o}: cc {src}\n'
374+
flags = cflags
375+
else:
376+
out += f'build {o}: cxx {src}\n'
377+
flags = cflags
378+
custom_flags = self.customize_build_flags(src, flags)
379+
if custom_flags != flags:
380+
out += f' CFLAGS = {join(custom_flags)}'
381+
out += '\n'
382+
383+
objects = ' '.join(objects)
384+
out += f'build {libname}: archive {objects}\n'
385+
386+
if not os.path.exists(filename) or utils.read_file(filename) != out:
387+
utils.write_file(filename, out)
388+
284389
def build_objects(self, build_dir):
285390
"""
286391
Returns a list of compiled object files for this library.
@@ -307,40 +412,52 @@ def build_objects(self, build_dir):
307412
o = os.path.join(build_dir, f'{object_basename}__{object_uuid}.o')
308413
ext = shared.suffix(src)
309414
if ext in ('.s', '.S', '.c'):
310-
cmd = [shared.EMCC]
415+
compiler = shared.EMCC
311416
else:
312-
cmd = [shared.EMXX]
417+
compiler = shared.EMXX
313418

314419
if ext == '.s':
315420
# .s files are processed directly by the assembler. In this case we can't pass
316421
# pre-processor flags such as `-I` and `-D` but we still want core flags such as
317422
# `-sMEMORY64`.
318-
cmd += get_base_cflags()
423+
flags = get_base_cflags()
319424
else:
320-
cmd += cflags
425+
flags = cflags
321426
if ext in ('.s', '.S'):
322427
# TODO(sbc) There is an llvm bug that causes a crash when `-g` is used with
323428
# assembly files that define wasm globals.
324-
cmd = [arg for arg in cmd if arg != '-g']
325-
cmd = self.customize_build_cmd(cmd, src)
326-
commands.append(cmd + ['-c', src, '-o', o])
429+
flags = [arg for arg in flags if arg != '-g']
430+
flags = self.customize_build_flags(src, flags)
431+
commands.append([compiler] + flags + ['-c', src, '-o', o])
327432
objects.append(o)
328433
run_build_commands(commands)
434+
logger.info('compiled %d inputs' % len(objects))
329435
return objects
330436

331-
def customize_build_cmd(self, cmd, filename): # noqa
437+
def customize_build_flags(self, filename, flags): # noqa
332438
"""Allows libraries to customize the build command used on per-file basis.
333439
334440
For example, libc uses this to replace -Oz with -O2 for some subset of files."""
335-
return cmd
441+
return flags
336442

337-
def build(self, out_filename):
443+
def do_build(self, out_filename):
338444
"""Builds the library and returns the path to the file."""
339-
build_dir = shared.Cache.get_path(os.path.join('build', self.get_base_name()))
340-
utils.safe_ensure_dirs(build_dir)
341-
create_lib(out_filename, self.build_objects(build_dir))
342-
if not shared.DEBUG:
343-
utils.delete_dir(build_dir)
445+
assert out_filename == self.get_path(absolute=True)
446+
build_dir = os.path.join(shared.Cache.get_path('build'), self.get_base_name())
447+
if USE_NINJA:
448+
ensure_sysroot()
449+
utils.safe_ensure_dirs(build_dir)
450+
ninja_file = os.path.join(build_dir, 'build.ninja')
451+
self.write_ninja_file(ninja_file, out_filename)
452+
shared.check_call(['ninja', '-C', build_dir], env=clean_env())
453+
else:
454+
# Use a seperate build directory to the ninja flavor so that building without
455+
# EMCC_USE_NINJA doesn't clobber the ninja build tree
456+
build_dir += '-tmp'
457+
utils.safe_ensure_dirs(build_dir)
458+
create_lib(out_filename, self.build_objects(build_dir))
459+
if not shared.DEBUG:
460+
utils.delete_dir(build_dir)
344461

345462
@classmethod
346463
def _inherit_list(cls, attr):
@@ -1002,18 +1119,18 @@ def get_files(self):
10021119

10031120
return libc_files
10041121

1005-
def customize_build_cmd(self, cmd, filename):
1006-
if filename in self.non_lto_files:
1122+
def customize_build_flags(self, filename, flags):
1123+
if filename not in self.non_lto_files:
10071124
# These files act more like the part of compiler-rt in that
10081125
# references to them can be generated at compile time.
10091126
# Treat them like compiler-rt in as much as never compile
10101127
# them as LTO and build them with -O2 rather then -Os (which
10111128
# use used for the rest of libc) because this set of files
10121129
# also contains performance sensitive math functions.
1013-
cmd = [a for a in cmd if not a.startswith('-flto')]
1014-
cmd = [a for a in cmd if not a.startswith('-O')]
1015-
cmd += ['-O2']
1016-
return cmd
1130+
flags = [a for a in flags if not a.startswith('-flto')]
1131+
flags = [a for a in flags if not a.startswith('-O')]
1132+
flags += ['-O2']
1133+
return flags
10171134

10181135

10191136
# Contains the files from libc that are optimized differently in -Oz mode, where
@@ -1029,7 +1146,7 @@ def __init__(self, **kwargs):
10291146
self.non_lto_files = self.get_libcall_files()
10301147

10311148
def get_libcall_files(self):
1032-
# see comments in libc.customize_build_cmd
1149+
# see comments in libc.customize_build_flags
10331150

10341151
# some files also appear in libc, and a #define affects them
10351152
mem_files = files_in_path(
@@ -1054,13 +1171,13 @@ def get_files(self):
10541171

10551172
return libcall_files + mem_files
10561173

1057-
def customize_build_cmd(self, cmd, filename):
1174+
def customize_build_flags(self, filename, flags):
10581175
if filename in self.non_lto_files:
1059-
# see comments in libc.customize_build_cmd
1060-
cmd = [a for a in cmd if not a.startswith('-flto')]
1061-
cmd = [a for a in cmd if not a.startswith('-O')]
1062-
cmd += ['-O2']
1063-
return cmd
1176+
# see comments in libc.customize_build_flags
1177+
flags = [f for f in flags if not f.startswith('-flto')]
1178+
flags = [f for f in flags if not f.startswith('-O')]
1179+
flags += ['-O2']
1180+
return flags
10641181

10651182
def can_use(self):
10661183
# Because libc_optz overrides parts of libc, it is not compatible with

0 commit comments

Comments
 (0)