Skip to content

Commit 5307118

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 e98554f commit 5307118

13 files changed

+188
-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/ports/__init__.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,27 @@ def build_port(src_dir, output_path, build_dir, includes=[], flags=[], exclude_f
129129
if not os.path.exists(build_dir):
130130
os.makedirs(build_dir)
131131
build_dir = src_dir
132-
commands = []
133-
objects = []
134-
for src in srcs:
135-
relpath = os.path.relpath(src, src_dir)
136-
obj = os.path.join(build_dir, relpath) + '.o'
137-
commands.append([shared.EMCC, '-c', src, '-o', obj] + cflags)
138-
objects.append(obj)
139-
140-
system_libs.run_build_commands(commands)
141-
system_libs.create_lib(output_path, objects)
132+
133+
if system_libs.USE_NINJA:
134+
ninja_file = os.path.join(build_dir, 'build.ninja')
135+
system_libs.ensure_sysroot()
136+
system_libs.create_ninja_file(srcs, ninja_file, output_path, cflags=cflags)
137+
cmd = ['ninja', '-C', build_dir]
138+
if shared.PRINT_STAGES:
139+
cmd.append('-v')
140+
shared.check_call(cmd, env=system_libs.clean_env())
141+
else:
142+
commands = []
143+
objects = []
144+
for src in srcs:
145+
relpath = os.path.relpath(src, src_dir)
146+
obj = os.path.join(build_dir, relpath) + '.o'
147+
commands.append([shared.EMCC, '-c', src, '-o', obj] + cflags)
148+
objects.append(obj)
149+
150+
system_libs.run_build_commands(commands)
151+
system_libs.create_lib(output_path, objects)
152+
142153
return output_path
143154

144155
@staticmethod
@@ -271,9 +282,16 @@ def clear_project_build(name):
271282
port = ports_by_name[name]
272283
port.clear(Ports, settings, shared)
273284
build_dir = os.path.join(Ports.get_build_dir(), name)
274-
utils.delete_dir(build_dir)
285+
if not system_libs.USE_NINJA:
286+
utils.delete_dir(build_dir)
275287
return build_dir
276288

289+
@staticmethod
290+
def write_file(filename, contents):
291+
if os.path.exists(filename) and utils.read_file(filename) == contents:
292+
return
293+
utils.write_file(filename, contents)
294+
277295

278296
def dependency_order(port_list):
279297
# Perform topological sort of ports according to the dependency DAG

tools/ports/boost_headers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import logging
77
import os
8-
from pathlib import Path
98

109
TAG = '1.75.0'
1110
HASH = '8c38be1ebef1b8ada358ad6b7c9ec17f5e0a300e8085db3473a13e19712c95eeb3c3defacd3c53482eb96368987c4b022efa8da2aac2431a154e40153d3c3dcd'
@@ -31,7 +30,7 @@ def create(final):
3130
build_dir = ports.clear_project_build('boost_headers')
3231
dummy_file = os.path.join(build_dir, 'dummy.cpp')
3332
shared.safe_ensure_dirs(os.path.dirname(dummy_file))
34-
Path(dummy_file).write_text('static void dummy() {}')
33+
ports.write_file(dummy_file, 'static void dummy() {}')
3534

3635
ports.build_port(build_dir, final, build_dir)
3736

tools/ports/freetype.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def get(ports, settings, shared):
1919

2020
def create(final):
2121
source_path = os.path.join(ports.get_dir(), 'freetype', 'FreeType-' + TAG)
22-
Path(source_path, 'include/ftconfig.h').write_text(ftconf_h)
22+
ports.write_file(Path(source_path, 'include/ftconfig.h'), ftconf_h)
2323
ports.install_header_dir(os.path.join(source_path, 'include'),
2424
target=os.path.join('freetype2'))
2525

tools/ports/libjpeg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def create(final):
2525
logging.info('building port: libjpeg')
2626
source_path = os.path.join(ports.get_dir(), 'libjpeg', 'jpeg-9c')
2727
dest_path = ports.clear_project_build('libjpeg')
28-
Path(source_path, 'jconfig.h').write_text(jconfig_h)
28+
ports.write_file(Path(source_path, 'jconfig.h'), jconfig_h)
2929
ports.install_headers(source_path)
3030
excludes = [
3131
'ansi2knr.c', 'cjpeg.c', 'ckconfig.c', 'djpeg.c', 'example.c',

tools/ports/libmodplug.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def create(final):
2525
src_dir = os.path.join(source_path, 'src')
2626
libmodplug_path = os.path.join(src_dir, 'libmodplug')
2727

28-
Path(source_path, 'config.h').write_text(config_h)
28+
ports.write_file(Path(source_path, 'config.h'), config_h)
2929

3030
flags = [
3131
'-Wno-deprecated-register',

tools/ports/libpng.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def create(final):
3030
logging.info('building port: libpng')
3131

3232
source_path = os.path.join(ports.get_dir(), 'libpng', 'libpng-' + TAG)
33-
Path(source_path, 'pnglibconf.h').write_text(pnglibconf_h)
33+
ports.write_file(Path(source_path, 'pnglibconf.h'), pnglibconf_h)
3434
ports.install_headers(source_path)
3535

3636
flags = ['-sUSE_ZLIB=1']

tools/ports/mpg123.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def create(final):
2727
libmpg123_path = os.path.join(src_path, 'libmpg123')
2828
compat_path = os.path.join(src_path, 'compat')
2929

30-
Path(src_path, 'config.h').write_text(config_h)
31-
Path(libmpg123_path, 'mpg123.h').write_text(mpg123_h)
30+
ports.write_file(Path(src_path, 'config.h'), config_h)
31+
ports.write_file(Path(libmpg123_path, 'mpg123.h'), mpg123_h)
3232

3333
# copy header to a location so it can be used as 'MPG123/'
3434
ports.install_headers(libmpg123_path, pattern="*123.h", target='')

tools/ports/ogg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def create(final):
2222
logging.info('building port: ogg')
2323

2424
source_path = os.path.join(ports.get_dir(), 'ogg', 'Ogg-' + TAG)
25-
Path(source_path, 'include', 'ogg', 'config_types.h').write_text(config_types_h)
25+
ports.write_file(Path(source_path, 'include', 'ogg', 'config_types.h'), config_types_h)
2626
ports.install_header_dir(os.path.join(source_path, 'include', 'ogg'), 'ogg')
2727
dest_path = ports.clear_project_build('ogg')
2828
ports.build_port(os.path.join(source_path, 'src'), final, dest_path)

tools/ports/zlib.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# found in the LICENSE file.
55

66
import os
7-
from pathlib import Path
87

98
VERSION = '1.2.12'
109
HASH = 'cc2366fa45d5dfee1f983c8c51515e0cff959b61471e2e8d24350dea22d3f6fcc50723615a911b046ffc95f51ba337d39ae402131a55e6d1541d3b095d6c0a14'
@@ -19,7 +18,7 @@ def get(ports, settings, shared):
1918

2019
def create(final):
2120
source_path = os.path.join(ports.get_dir(), 'zlib', 'zlib-' + VERSION)
22-
Path(source_path, 'zconf.h').write_text(zconf_h)
21+
ports.write_file(os.path.join(source_path, 'zconf.h'), zconf_h)
2322
ports.install_headers(source_path)
2423

2524
# build

tools/system_libs.py

Lines changed: 142 additions & 10 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 rebuild 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)
@@ -93,6 +101,107 @@ def create_lib(libname, inputs):
93101
building.emar('cr', libname, inputs)
94102

95103

104+
def create_ninja_file(input_files, filename, libname, cflags, asflags=None, customize_build_flags=None):
105+
if asflags is None:
106+
asflags = []
107+
# TODO(sbc) There is an llvm bug that causes a crash when `-g` is used with
108+
# assembly files that define wasm globals.
109+
asflags = [arg for arg in asflags if arg != '-g']
110+
cflags_asm = [arg for arg in cflags if arg != '-g']
111+
112+
def join(flags):
113+
return ' '.join(flags)
114+
115+
out = f'''\
116+
# Automatically generated by tools/system_libs.py. DO NOT EDIT
117+
118+
ninja_required_version = 1.5
119+
120+
ASFLAGS = {join(asflags)}
121+
CFLAGS = {join(cflags)}
122+
CFLAGS_ASM = {join(cflags_asm)}
123+
EMCC = {shared.EMCC}
124+
EMXX = {shared.EMXX}
125+
EMAR = {shared.EMAR}
126+
127+
rule cc
128+
depfile = $out.d
129+
command = $EMCC -MD -MF $out.d $CFLAGS -c $in -o $out
130+
description = CC $out
131+
132+
rule cxx
133+
depfile = $out.d
134+
command = $EMXX -MD -MF $out.d $CFLAGS -c $in -o $out
135+
description = CXX $out
136+
137+
rule asm
138+
command = $EMCC $ASFLAGS -c $in -o $out
139+
description = ASM $out
140+
141+
rule asm_cpp
142+
depfile = $out.d
143+
command = $EMCC -MD -MF $out.d $CFLAGS_ASM -c $in -o $out
144+
description = ASM $out
145+
146+
rule direct_cc
147+
depfile = $with_depfile
148+
command = $EMCC -MD -MF $with_depfile $CFLAGS -c $in -o $out
149+
description = CC $out
150+
151+
rule archive
152+
command = $EMAR cr $out $in
153+
description = AR $out
154+
155+
'''
156+
suffix = shared.suffix(libname)
157+
158+
case_insensitive = is_case_insensitive(os.path.dirname(filename))
159+
if suffix == '.o':
160+
assert len(input_files) == 1
161+
depfile = shared.unsuffixed_basename(input_files[0]) + '.d'
162+
out += f'build {libname}: direct_cc {input_files[0]}\n'
163+
out += f' with_depfile = {depfile}\n'
164+
else:
165+
objects = []
166+
for src in input_files:
167+
# Resolve duplicates by appending unique.
168+
# This is needed on case insensitve filesystem to handle,
169+
# for example, _exit.o and _Exit.o.
170+
o = shared.unsuffixed_basename(src) + '.o'
171+
object_uuid = 0
172+
if case_insensitive:
173+
o = o.lower()
174+
# Find a unique basename
175+
while o in objects:
176+
object_uuid += 1
177+
o = f'{o}__{object_uuid}.o'
178+
objects.append(o)
179+
ext = shared.suffix(src)
180+
if ext == '.s':
181+
out += f'build {o}: asm {src}\n'
182+
flags = asflags
183+
elif ext == '.S':
184+
out += f'build {o}: asm_cpp {src}\n'
185+
flags = cflags_asm
186+
elif ext == '.c':
187+
out += f'build {o}: cc {src}\n'
188+
flags = cflags
189+
else:
190+
out += f'build {o}: cxx {src}\n'
191+
flags = cflags
192+
if customize_build_flags:
193+
custom_flags = customize_build_flags(flags, src)
194+
if custom_flags != flags:
195+
out += f' CFLAGS = {join(custom_flags)}'
196+
out += '\n'
197+
198+
objects = sorted(objects, key=lambda x: os.path.basename(x))
199+
objects = ' '.join(objects)
200+
out += f'build {libname}: archive {objects}\n'
201+
202+
utils.write_file(filename, out)
203+
204+
96205
def is_case_insensitive(path):
97206
"""Returns True if the filesystem at `path` is case insensitive."""
98207
utils.write_file(os.path.join(path, 'test_file'), '')
@@ -240,23 +349,26 @@ def can_build(self):
240349
return True
241350

242351
def erase(self):
243-
shared.Cache.erase_file(shared.Cache.get_lib_name(self.get_filename()))
352+
shared.Cache.erase_file(self.get_path())
244353

245-
def get_path(self):
354+
def get_path(self, absolute=False):
355+
return shared.Cache.get_lib_name(self.get_filename(), absolute=absolute)
356+
357+
def build(self):
246358
"""
247359
Gets the cached path of this library.
248360
249361
This will trigger a build if this library is not in the cache.
250362
"""
251-
return shared.Cache.get_lib(self.get_filename(), self.build)
363+
return shared.Cache.get(self.get_path(), self.do_build, force=USE_NINJA == 2, quiet=USE_NINJA)
252364

253365
def get_link_flag(self):
254366
"""
255367
Gets the link flags needed to use the library.
256368
257369
This will trigger a build if this library is not in the cache.
258370
"""
259-
fullpath = self.get_path()
371+
fullpath = self.build()
260372
# For non-libaries (e.g. crt1.o) we pass the entire path to the linker
261373
if self.get_ext() != '.a':
262374
return fullpath
@@ -282,6 +394,12 @@ def get_files(self):
282394

283395
raise NotImplementedError()
284396

397+
def write_ninja_file(self, filename, libname):
398+
cflags = self.get_cflags()
399+
asflags = get_base_cflags()
400+
input_files = self.get_files()
401+
create_ninja_file(input_files, filename, libname, cflags, asflags=asflags, customize_build_flags=self.customize_build_cmd)
402+
285403
def build_objects(self, build_dir):
286404
"""
287405
Returns a list of compiled object files for this library.
@@ -335,13 +453,27 @@ def customize_build_cmd(self, cmd, filename): # noqa
335453
For example, libc uses this to replace -Oz with -O2 for some subset of files."""
336454
return cmd
337455

338-
def build(self, out_filename):
456+
def do_build(self, out_filename):
339457
"""Builds the library and returns the path to the file."""
340-
build_dir = shared.Cache.get_path(os.path.join('build', self.get_base_name()))
341-
utils.safe_ensure_dirs(build_dir)
342-
create_lib(out_filename, self.build_objects(build_dir))
343-
if not shared.DEBUG:
344-
utils.delete_dir(build_dir)
458+
assert out_filename == self.get_path(absolute=True)
459+
build_dir = os.path.join(shared.Cache.get_path('build'), self.get_base_name())
460+
if USE_NINJA:
461+
ensure_sysroot()
462+
utils.safe_ensure_dirs(build_dir)
463+
ninja_file = os.path.join(build_dir, 'build.ninja')
464+
self.write_ninja_file(ninja_file, out_filename)
465+
cmd = ['ninja', '-C', build_dir]
466+
if shared.PRINT_STAGES:
467+
cmd.append('-v')
468+
shared.check_call(cmd, env=clean_env())
469+
else:
470+
# Use a seperate build directory to the ninja flavor so that building without
471+
# EMCC_USE_NINJA doesn't clobber the ninja build tree
472+
build_dir += '-tmp'
473+
utils.safe_ensure_dirs(build_dir)
474+
create_lib(out_filename, self.build_objects(build_dir))
475+
if not shared.DEBUG:
476+
utils.delete_dir(build_dir)
345477

346478
@classmethod
347479
def _inherit_list(cls, attr):

0 commit comments

Comments
 (0)