Skip to content

Commit

Permalink
Experimental support for bulding system libraries via ninja
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sbc100 committed Sep 14, 2022
1 parent cdbbefa commit 27b49d5
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 28 deletions.
2 changes: 1 addition & 1 deletion embuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def main():
if do_clear:
library.erase()
if do_build:
library.get_path()
library.build()
elif what == 'sysroot':
if do_clear:
shared.Cache.erase_file('sysroot_install.stamp')
Expand Down
11 changes: 6 additions & 5 deletions tools/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ def get_lib_dir(self, absolute, varies=True):
path = Path(path, '-'.join(subdir))
return path

def get_lib_name(self, name, varies=True):
return str(self.get_lib_dir(absolute=False, varies=varies).joinpath(name))
def get_lib_name(self, name, varies=True, absolute=False):
return str(self.get_lib_dir(absolute=absolute, varies=varies).joinpath(name))

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

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

return str(cachename)
2 changes: 1 addition & 1 deletion tools/gen_struct_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def inspect_headers(headers, cflags):
# TODO(sbc): If we can remove EM_EXCLUSIVE_CACHE_ACCESS then this would not longer be needed.
shared.check_sanity()

compiler_rt = system_libs.Library.get_usable_variations()['libcompiler_rt'].get_path()
compiler_rt = system_libs.Library.get_usable_variations()['libcompiler_rt'].build()

# Close all unneeded FDs.
os.close(src_file[0])
Expand Down
37 changes: 26 additions & 11 deletions tools/ports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,24 @@ def build_port(src_dir, output_path, build_dir, includes=[], flags=[], exclude_f
if not os.path.exists(build_dir):
os.makedirs(build_dir)
build_dir = src_dir
commands = []
objects = []
for src in srcs:
relpath = os.path.relpath(src, src_dir)
obj = os.path.join(build_dir, relpath) + '.o'
commands.append([shared.EMCC, '-c', src, '-o', obj] + cflags)
objects.append(obj)

system_libs.run_build_commands(commands)
system_libs.create_lib(output_path, objects)

if system_libs.USE_NINJA:
ninja_file = os.path.join(build_dir, 'build.ninja')
system_libs.ensure_sysroot()
system_libs.create_ninja_file(srcs, ninja_file, output_path, cflags=cflags)
shared.check_call(['ninja', '-C', build_dir], env=system_libs.clean_env())
else:
commands = []
objects = []
for src in srcs:
relpath = os.path.relpath(src, src_dir)
obj = os.path.join(build_dir, relpath) + '.o'
commands.append([shared.EMCC, '-c', src, '-o', obj] + cflags)
objects.append(obj)

system_libs.run_build_commands(commands)
system_libs.create_lib(output_path, objects)

return output_path

@staticmethod
Expand Down Expand Up @@ -271,9 +279,16 @@ def clear_project_build(name):
port = ports_by_name[name]
port.clear(Ports, settings, shared)
build_dir = os.path.join(Ports.get_build_dir(), name)
utils.delete_dir(build_dir)
if not system_libs.USE_NINJA:
utils.delete_dir(build_dir)
return build_dir

@staticmethod
def write_file(filename, contents):
if os.path.exists(filename) and utils.read_file(filename) == contents:
return
utils.write_file(filename, contents)


def dependency_order(port_list):
# Perform topological sort of ports according to the dependency DAG
Expand Down
153 changes: 143 additions & 10 deletions tools/system_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
'getsockopt.c', 'setsockopt.c', 'freeaddrinfo.c',
'in6addr_any.c', 'in6addr_loopback.c', 'accept4.c']

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


def files_in_path(path, filenames):
srcdir = utils.path_from_root(path)
Expand Down Expand Up @@ -93,6 +101,108 @@ def create_lib(libname, inputs):
building.emar('cr', libname, inputs)


def create_ninja_file(input_files, filename, libname, cflags, asflags=None, customize_build_flags=None):
if asflags is None:
asflags = []
# TODO(sbc) There is an llvm bug that causes a crash when `-g` is used with
# assembly files that define wasm globals.
asflags = [arg for arg in asflags if arg != '-g']
cflags_asm = [arg for arg in cflags if arg != '-g']

def join(flags):
return ' '.join(flags)

out = f'''\
# Automatically generated by tools/system_libs.py. DO NOT EDIT
ninja_required_version = 1.5
ASFLAGS = {join(asflags)}
CFLAGS = {join(cflags)}
CFLAGS_ASM = {join(cflags_asm)}
EMCC = {shared.EMCC}
EMXX = {shared.EMXX}
EMAR = {shared.EMAR}
rule cc
depfile = $out.d
command = $EMCC -MD -MF $out.d $CFLAGS -c $in -o $out
description = CC $out
rule cxx
depfile = $out.d
command = $EMXX -MD -MF $out.d $CFLAGS -c $in -o $out
description = CXX $out
rule asm
command = $EMCC $ASFLAGS -c $in -o $out
description = ASM $out
rule asm_cpp
depfile = $out.d
command = $EMCC -MD -MF $out.d $CFLAGS_ASM -c $in -o $out
description = ASM $out
rule direct_cc
depfile = $with_depfile
command = $EMCC -MD -MF $with_depfile $CFLAGS -c $in -o $out
description = CC $out
rule archive
command = $EMAR cr $out $in
description = AR $out
'''
suffix = shared.suffix(libname)

if suffix == '.o':
assert len(input_files) == 1
depfile = shared.unsuffixed_basename(input_files[0]) + '.d'
out += f'build {libname}: direct_cc {input_files[0]}\n'
out += f' with_depfile = {depfile}\n'
else:
case_insensitive = True #is_case_insensitive(build_dir)
objects = []
for src in input_files:
# Resolve duplicates by appending unique.
# This is needed on case insensitve filesystem to handle,
# for example, _exit.o and _Exit.o.
o = shared.unsuffixed_basename(src) + '.o'
object_uuid = 0
if case_insensitive:
o = o.lower()
# Find a unique basename
while o in objects:
object_uuid += 1
o = f'{o}__{object_uuid}.o'
objects.append(o)
ext = shared.suffix(src)
if ext == '.s':
out += f'build {o}: asm {src}\n'
flags = asflags
elif ext == '.S':
out += f'build {o}: asm_cpp {src}\n'
flags = cflags_asm
elif ext == '.c':
out += f'build {o}: cc {src}\n'
flags = cflags
else:
out += f'build {o}: cxx {src}\n'
flags = cflags
if customize_build_flags:
custom_flags = customize_build_flags(flags, src)
if custom_flags != flags:
out += f' CFLAGS = {join(custom_flags)}'
out += '\n'

objects = sorted(objects, key=lambda x: os.path.basename(x))
objects = ' '.join(objects)
out += f'build {libname}: archive {objects}\n'

if not os.path.exists(filename) or utils.read_file(filename) != out:
utils.write_file(filename, out)


def is_case_insensitive(path):
"""Returns True if the filesystem at `path` is case insensitive."""
utils.write_file(os.path.join(path, 'test_file'), '')
Expand Down Expand Up @@ -240,23 +350,26 @@ def can_build(self):
return True

def erase(self):
shared.Cache.erase_file(shared.Cache.get_lib_name(self.get_filename()))
shared.Cache.erase_file(self.get_path())

def get_path(self):
def get_path(self, absolute=False):
return shared.Cache.get_lib_name(self.get_filename(), absolute=absolute)

def build(self):
"""
Gets the cached path of this library.
This will trigger a build if this library is not in the cache.
"""
return shared.Cache.get_lib(self.get_filename(), self.build)
return shared.Cache.get(self.get_path(), self.do_build, force=USE_NINJA == 2, quiet=USE_NINJA)

def get_link_flag(self):
"""
Gets the link flags needed to use the library.
This will trigger a build if this library is not in the cache.
"""
fullpath = self.get_path()
fullpath = self.build()
# For non-libaries (e.g. crt1.o) we pass the entire path to the linker
if self.get_ext() != '.a':
return fullpath
Expand All @@ -282,6 +395,12 @@ def get_files(self):

raise NotImplementedError()

def write_ninja_file(self, filename, libname):
cflags = self.get_cflags()
asflags = get_base_cflags()
input_files = self.get_files()
create_ninja_file(input_files, filename, libname, cflags, asflags=asflags, customize_build_flags=self.customize_build_cmd)

def build_objects(self, build_dir):
"""
Returns a list of compiled object files for this library.
Expand Down Expand Up @@ -335,13 +454,27 @@ def customize_build_cmd(self, cmd, filename): # noqa
For example, libc uses this to replace -Oz with -O2 for some subset of files."""
return cmd

def build(self, out_filename):
def do_build(self, out_filename):
"""Builds the library and returns the path to the file."""
build_dir = shared.Cache.get_path(os.path.join('build', self.get_base_name()))
utils.safe_ensure_dirs(build_dir)
create_lib(out_filename, self.build_objects(build_dir))
if not shared.DEBUG:
utils.delete_dir(build_dir)
assert out_filename == self.get_path(absolute=True)
build_dir = os.path.join(shared.Cache.get_path('build'), self.get_base_name())
if USE_NINJA:
ensure_sysroot()
utils.safe_ensure_dirs(build_dir)
ninja_file = os.path.join(build_dir, 'build.ninja')
self.write_ninja_file(ninja_file, out_filename)
cmd = ['ninja', '-C', build_dir]
if shared.PRINT_STAGES:
cmd.append('-v')
shared.check_call(cmd, env=clean_env())
else:
# Use a seperate build directory to the ninja flavor so that building without
# EMCC_USE_NINJA doesn't clobber the ninja build tree
build_dir += '-tmp'
utils.safe_ensure_dirs(build_dir)
create_lib(out_filename, self.build_objects(build_dir))
if not shared.DEBUG:
utils.delete_dir(build_dir)

@classmethod
def _inherit_list(cls, attr):
Expand Down

0 comments on commit 27b49d5

Please sign in to comment.