| #!/usr/bin/env python3.8 |
| # Copyright 2017 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # Build script for a Go app. |
| |
| import argparse |
| import os |
| import subprocess |
| import sys |
| import string |
| import shutil |
| import errno |
| |
| from gen_library_metadata import get_sources |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--godepfile', help='Path to godepfile tool', required=True) |
| parser.add_argument( |
| '--root-out-dir', help='Path to root of build output', required=True) |
| parser.add_argument( |
| '--cc', help='The C compiler to use', required=False, default='cc') |
| parser.add_argument( |
| '--cxx', help='The C++ compiler to use', required=False, default='c++') |
| parser.add_argument( |
| '--dump-syms', help='The dump_syms tool to use', required=False) |
| parser.add_argument( |
| '--objcopy', |
| help='The objcopy tool to use', |
| required=False, |
| default='objcopy') |
| parser.add_argument('--sysroot', help='The sysroot to use', required=False) |
| parser.add_argument( |
| '--target', help='The compiler target to use', required=False) |
| parser.add_argument( |
| '--depfile', help='The path to the depfile', required=False) |
| parser.add_argument( |
| '--current-cpu', |
| help='Target architecture.', |
| choices=['x64', 'arm64'], |
| required=True) |
| parser.add_argument( |
| '--current-os', |
| help='Target operating system.', |
| choices=['fuchsia', 'linux', 'mac', 'win'], |
| required=True) |
| parser.add_argument('--buildidtool', help='The path to the buildidtool.') |
| parser.add_argument( |
| '--build-id-dir', help='The path to the .build-id directory.') |
| parser.add_argument( |
| '--go-root', help='The go root to use for builds.', required=True) |
| parser.add_argument( |
| '--go-cache', help='Cache directory to use for builds.', required=False) |
| parser.add_argument( |
| '--is-test', help='True if the target is a go test', default=False) |
| parser.add_argument('--buildmode', help='Build mode to use') |
| parser.add_argument( |
| '--gcflag', |
| help='Arguments to pass to Go compiler', |
| action='append', |
| default=[]) |
| parser.add_argument( |
| '--ldflag', |
| help='Arguments to pass to Go linker', |
| action='append', |
| default=[]) |
| parser.add_argument( |
| '--go-dep-files', |
| help='List of files describing library dependencies', |
| nargs='*', |
| default=[]) |
| parser.add_argument( |
| '--root-build-dir', |
| help='Root build directory. Required if --go-dep-files is used.') |
| parser.add_argument('--binname', help='Output file', required=True) |
| parser.add_argument( |
| '--output-path', |
| help='Where to output the (unstripped) binary', |
| required=True) |
| parser.add_argument( |
| '--stripped-output-path', |
| help='Where to output a stripped binary, if supplied') |
| parser.add_argument( |
| '--verbose', |
| help='Tell the go tool to be verbose about what it is doing', |
| action='store_true') |
| parser.add_argument('--package', help='The package name', required=True) |
| parser.add_argument( |
| '--include-dir', |
| help='-isystem path to add', |
| action='append', |
| default=[]) |
| parser.add_argument( |
| '--lib-dir', help='-L path to add', action='append', default=[]) |
| parser.add_argument('--vet', help='Run go vet', action='store_true') |
| parser.add_argument( |
| '--tag', help='Add a go build tag', default=[], action='append') |
| parser.add_argument( |
| '--cgo', help='Whether to enable CGo', action='store_true') |
| args = parser.parse_args() |
| |
| try: |
| os.makedirs(args.go_cache) |
| except OSError as e: |
| if e.errno == errno.EEXIST and os.path.isdir(args.go_cache): |
| pass |
| else: |
| raise |
| |
| goarch = { |
| 'x64': 'amd64', |
| 'arm64': 'arm64', |
| }[args.current_cpu] |
| goos = { |
| 'fuchsia': 'fuchsia', |
| 'linux': 'linux', |
| 'mac': 'darwin', |
| 'win': 'windows', |
| }[args.current_os] |
| |
| build_id_dir = os.path.join(args.root_out_dir, '.build-id') |
| dist = args.stripped_output_path or args.output_path |
| |
| # Project path is a package specific gopath, also known as a "project" in go parlance. |
| project_path = os.path.join( |
| args.root_out_dir, 'gen', 'gopaths', args.binname) |
| |
| # Clean up any old project path to avoid leaking old dependencies. |
| gopath_src = os.path.join(project_path, 'src') |
| if os.path.exists(gopath_src): |
| shutil.rmtree(gopath_src) |
| os.makedirs(gopath_src) |
| |
| link_to_source_list = [] |
| if args.go_dep_files: |
| assert args.root_build_dir, ( |
| '--root-build-dir is required with --go-dep-files') |
| |
| root_build_dir = os.path.abspath(args.root_build_dir) |
| link_to_source = {} |
| |
| # Create a GOPATH for the packages dependency tree. |
| for dst, src in sorted(get_sources(args.go_dep_files).items()): |
| # Determine if the src path should |
| # - be mapped as-is which, if src is a directory, includes all subdirectories |
| # - have its contents enumerated and mapped directly |
| map_directly = False |
| |
| if dst.endswith('/...'): |
| # When a directory and all its subdirectories must be made available, map |
| # the directory directly. |
| map_directly = True |
| |
| # - src can have a '/...' suffix like with 'github.com/google/go-cmp/...'. |
| # - This means all subpackages are being symlinked to the GOPATH. |
| # - dst have the suffix when defining a package. |
| # - src can only have the suffix if dst has it too. |
| assert dst.endswith('/...') >= src.endswith('/...'), (dst, src) |
| dst = dst[:-4] |
| if src.endswith('/...'): |
| src = src[:-4] |
| elif os.path.isfile(src): |
| # When sources are explicitly listed in the BUILD.gn file, each `src` will |
| # be a path to a file that must be mapped directly. |
| map_directly = True |
| # Paths with /.../ in the middle designate go packages that include |
| # subpackages, but also explicitly list all their source files. |
| # The construction of these paths is done in the |
| # godepfile tool, so we remove these sentinel values here. |
| dst = dst.replace('/.../', '/') |
| |
| dstdir = os.path.join(gopath_src, dst) |
| |
| if map_directly: |
| # Make a symlink to the src directory or file. |
| parent = os.path.dirname(dstdir) |
| if not os.path.exists(parent): |
| os.makedirs(parent) |
| os.symlink(src, dstdir) |
| link_to_source[os.path.join(root_build_dir, dstdir)] = src |
| else: |
| # Map individual files since the dependency is only on the |
| # package itself, not Go subpackages. The only exception is |
| # 'testdata'. |
| os.makedirs(dstdir) |
| for filename in os.listdir(src): |
| src_file = os.path.join(src, filename) |
| if filename == 'testdata' or os.path.isfile(src_file): |
| os.symlink(src_file, os.path.join(dstdir, filename)) |
| link_to_source[os.path.join( |
| root_build_dir, dstdir, filename)] = src |
| |
| # Create a sorted list of (link, src) pairs, with longest paths before |
| # short one. This ensures that 'foobar' will appear before 'foo'. |
| link_to_source_list = sorted( |
| link_to_source.items(), key=lambda x: x[0], reverse=True) |
| |
| cflags = [] |
| if args.sysroot: |
| cflags.extend(['--sysroot', args.sysroot]) |
| if args.target: |
| cflags.extend(['-target', args.target]) |
| |
| ldflags = cflags[:] |
| if args.current_os == 'linux': |
| ldflags.extend( |
| [ |
| '-stdlib=libc++', |
| # TODO(fxbug.dev/64336): the following flags are not recognized by CGo. |
| # '-rtlib=compiler-rt', |
| # '-unwindlib=', |
| ]) |
| |
| for dir in args.include_dir: |
| cflags.extend(['-isystem', dir]) |
| ldflags.extend(['-L' + dir for dir in args.lib_dir]) |
| |
| if args.sysroot: |
| ldflags.extend(['-L' + args.sysroot + '/lib']) |
| |
| cflags_joined = ' '.join(cflags) |
| ldflags_joined = ' '.join(ldflags) |
| |
| gopath = os.path.abspath(project_path) |
| build_goroot = os.path.abspath(args.go_root) |
| |
| env = { |
| # /usr/bin:/bin are required for basic things like bash(1) and env(1). Note |
| # that on Mac, ld is also found from /usr/bin. |
| 'PATH': os.path.join(build_goroot, 'bin') + ':/usr/bin:/bin', |
| # Disable modules to ensure Go doesn't try to download dependencies. |
| 'GO111MODULE': 'off', |
| 'GOARCH': goarch, |
| 'GOOS': goos, |
| 'GOPATH': gopath, |
| # Some users have GOROOT set in their parent environment, which can break |
| # things, so it is always set explicitly here. |
| 'GOROOT': build_goroot, |
| 'GOCACHE': args.go_cache, |
| 'CC': args.cc, |
| 'CXX': args.cxx, |
| 'CGO_CFLAGS': cflags_joined, |
| 'CGO_CPPFLAGS': cflags_joined, |
| 'CGO_CXXFLAGS': cflags_joined, |
| 'CGO_LDFLAGS': ldflags_joined, |
| } |
| |
| # Infra sets $TMPDIR which is cleaned between builds. |
| if os.getenv('TMPDIR'): |
| env['TMPDIR'] = os.getenv('TMPDIR') |
| |
| if args.cgo: |
| env['CGO_ENABLED'] = '1' |
| if args.target: |
| env['CC_FOR_TARGET'] = env['CC'] |
| env['CXX_FOR_TARGET'] = env['CXX'] |
| |
| go_tool = os.path.join(build_goroot, 'bin', 'go') |
| |
| if args.vet: |
| retcode = subprocess.call([go_tool, 'vet', args.package], env=env) |
| if retcode != 0: |
| return retcode |
| |
| cmd = [go_tool] |
| if args.is_test: |
| cmd += ['test', '-c'] |
| else: |
| cmd += ['build', '-trimpath'] |
| if args.verbose: |
| cmd += ['-x'] |
| if args.tag: |
| # Separate tags by spaces. This behavior is actually deprecated in the |
| # go command line, but Fuchsia currently has an older version of go |
| # that hasn't switched to commas. |
| cmd += ['-tags', ' '.join(args.tag)] |
| if args.buildmode: |
| cmd += ['-buildmode', args.buildmode] |
| if args.gcflag: |
| cmd += ['-gcflags', ' '.join(args.gcflag)] |
| if args.ldflag: |
| cmd += ['-ldflags=' + ' '.join(args.ldflag)] |
| cmd += [ |
| '-pkgdir', |
| os.path.join(project_path, 'pkg'), |
| '-o', |
| args.output_path, |
| args.package, |
| ] |
| retcode = subprocess.call(cmd, env=env) |
| |
| if retcode == 0 and args.stripped_output_path: |
| if args.current_os == 'mac': |
| retcode = subprocess.call( |
| [ |
| 'xcrun', 'strip', '-x', args.output_path, '-o', |
| args.stripped_output_path |
| ], |
| env=env) |
| else: |
| retcode = subprocess.call( |
| [ |
| args.objcopy, '--strip-sections', args.output_path, |
| args.stripped_output_path |
| ], |
| env=env) |
| |
| # TODO(fxbug.dev/27215): Also invoke the buildidtool in the case of linux |
| # once buildidtool knows how to deal in Go's native build ID format. |
| supports_build_id = args.current_os == 'fuchsia' |
| if retcode == 0 and args.dump_syms and supports_build_id: |
| if args.current_os == 'fuchsia': |
| with open(dist + '.sym', 'w') as f: |
| retcode = subprocess.call( |
| [args.dump_syms, '-r', '-o', 'Fuchsia', args.output_path], |
| stdout=f) |
| |
| if retcode == 0 and args.buildidtool and supports_build_id: |
| if not args.build_id_dir: |
| raise ValueError('Using --buildidtool requires --build-id-dir') |
| retcode = subprocess.call( |
| [ |
| args.buildidtool, |
| '-build-id-dir', |
| args.build_id_dir, |
| '-stamp', |
| dist + '.build-id.stamp', |
| '-entry', |
| '.debug=' + args.output_path, |
| '-entry', |
| '=' + dist, |
| ]) |
| |
| if retcode == 0: |
| if args.depfile is not None: |
| godepfile_args = [args.godepfile, '-o', dist] |
| for f, t in link_to_source_list: |
| godepfile_args += ['-prefixmap', '%s=%s' % (f, t)] |
| if args.is_test: |
| godepfile_args += ['-test'] |
| godepfile_args += [args.package] |
| with open(args.depfile, 'wb') as into: |
| subprocess.check_call(godepfile_args, env=env, stdout=into) |
| |
| return retcode |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |