blob: 51f7fccc3fab2e47cbc66a041f025e1bc047a261 [file] [log] [blame]
#! /usr/bin/env python
# Copyright 2020 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import abc
import argparse
import glob
import multiprocessing
import os
import os.path
import pickle
import shutil
import six
import subprocess
import textwrap
SETTINGS_FILE = '.build_cross_gcc.settings'
LOG_FILE = 'build_cross_gcc.log'
all_settings = {}
all_steps = {}
description_paragraphs = [
'''
This script helps automate building a gcc based cross compiler.
The process is broken down into a series of steps which can be
executed one at a time or in arbtitrary sequences. It's assumed that
you've already downloaded the following sources into the current
directory:''',
'',
'''1. binutils''',
'''2. gcc''',
'''3. glibc''',
'''4. linux kernel''',
'''5. gdb''',
'',
'''
The entire process can be configured with a series of settings
which are stored in a config file called {settings_file}. These
settings can generally also be set from the command line, and at run
time using step 0 of the process. Many will set themselves to
reasonable defaults if no value was loaded from a previous
configuration or a saved settings file.''',
'',
'''
Prebaked config options can be loaded in from an external file to
make it easier to build particular cross compilers without having to
mess with a lot of options.'''
'',
'''
When settings are listed, any setting which has a value which has
failed validation or which hasn't been set and doesn't have a
reasonable default will be marked with a X in the far left hand
column. Settings will generally refuse to be set to invalid values,
unless they were like that by default and the user refused to correct
them.''',
'',
'''This script is based on the excellent how-to here:''',
'''https://preshing.com/20141119/how-to-build-a-gcc-cross-compiler/''',
'',
'''
Please view that webpage for a detailed explanation of what this
script does.'''
]
def help_text_wrapper(text):
width = shutil.get_terminal_size().columns
text = textwrap.dedent(text)
text = text.strip()
return textwrap.fill(text, width=width)
description = '\n'.join(list(map(help_text_wrapper, description_paragraphs)))
argparser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=description)
#
# Some helper utilities.
#
def confirm(prompt):
while True:
yn = input('{} (N/y): '.format(prompt))
if yn == '':
yn = 'n'
if yn.lower() in ('y', 'Yes'):
return True
elif yn.lower() in ('n', 'No'):
return False
def setup_build_dir(subdir):
build_dir_base = BuildDirBase.setting()
target = Target.setting()
if not (build_dir_base.valid and target.valid):
return False
target_build_dir = os.path.join(build_dir_base.get(), target.get())
build_dir = os.path.join(target_build_dir, 'build-{}'.format(subdir))
if not os.path.isdir(build_dir):
os.makedirs(build_dir)
return build_dir
def run_commands(working_dir, *cmds):
with open(LOG_FILE, 'a') as log:
print('In working directory {:s} (log in {:s}):'.format(
working_dir, LOG_FILE))
for cmd in cmds:
print(textwrap.fill(cmd, initial_indent=' ',
subsequent_indent=' ',
width=shutil.get_terminal_size().columns))
print('', file=log)
print(cmd, file=log)
print('', file=log)
if subprocess.call(cmd, shell=True, cwd=working_dir,
stdout=log, stderr=subprocess.STDOUT) != 0:
return False
return True
#
# Settings.
#
class MetaSetting(type):
def __new__(mcls, name, bases, d):
cls = super(MetaSetting, mcls).__new__(mcls, name, bases, d)
key = d.get('key', None)
if key is not None:
assert('default' in d)
instance = cls()
instance.value = None
instance.valid = False
all_settings[key] = instance
return cls
@six.add_metaclass(MetaSetting)
@six.add_metaclass(abc.ABCMeta)
class Setting(object):
key = None
@abc.abstractmethod
def set(self, value):
'Validate and set the setting to "value", and return if successful.'
self.value = value
self.valid = True
return True
def set_default(self):
'Set this setting to its default value, and return if successful.'
return self.set(self.default)
def set_arg(self, value):
'Set this setting to value if not None, and return if successful.'
if value:
return self.set(value)
else:
# Nothing happened, so nothing failed.
return True
def get(self):
'Return the value of this setting.'
return self.value
@abc.abstractmethod
def describe(self):
'Return a string describing this setting.'
return ''
@abc.abstractmethod
def add_to_argparser(self, argparser):
'Add command line options associated with this setting.'
@abc.abstractmethod
def set_from_args(self, args):
'Set this setting from the command line arguments, if requested.'
return True
@classmethod
def setting(cls):
s = all_settings[cls.key]
if not s.valid:
print('"{}" is not valid.'.format(s.key))
return s
class DirectorySetting(Setting):
def set(self, value):
if not os.path.exists(value):
print('Path "{:s}" does not exist.'.format(value))
elif not os.path.isdir(value):
print('Path "{:s}" is not a directory.'.format(value))
else:
self.value = value
self.valid = True
return self.valid
def set_default(self):
if not self.set(self.default):
if not os.path.exists(self.default):
if confirm('Create?'):
try:
os.mkdirs(value)
assert(self.set(self.default))
except:
print('Failed to make directory')
self.valid = False
return False
else:
self.value = self.default
self.valid = False
return False
class Prefix(DirectorySetting):
default = os.path.join(os.environ['HOME'], 'cross')
key = 'PREFIX'
def describe(self):
return 'Path prefix to install to.'
def add_to_argparser(self, parser):
parser.add_argument('--prefix', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.prefix)
class BuildDirBase(DirectorySetting):
default = os.getcwd()
key = 'BUILD_DIR_BASE'
def describe(self):
return 'Path prefix for build directory(ies).'
def add_to_argparser(self, parser):
parser.add_argument('--build-dir-base', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.build_dir_base)
class Target(Setting):
key = 'TARGET'
default = None
def set_default(self):
self.value = '(not set)'
self.valid = False
return False
def describe(self):
return 'Tuple for the target architecture.'
def add_to_argparser(self, parser):
parser.add_argument('--target', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.target)
class LinuxArch(Setting):
key = 'LINUX_ARCH'
default = None
def set_default(self):
self.value = '(not set)'
self.valid = False
return False
def describe(self):
return 'The arch directory for Linux headers.'
def add_to_argparser(self, parser):
parser.add_argument('--linux-arch', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.linux_arch)
class SourceDirSetting(Setting):
def set(self, value):
if os.path.isdir(value):
self.value = value
self.valid = True
return self.valid
def set_default(self):
matches = list(filter(os.path.isdir, glob.glob(self.pattern)))
if len(matches) == 0:
self.valid = False
return False
if len(matches) > 1:
while True:
print()
print('Multple options for "{:s}":'.format(self.key))
choices = list(enumerate(matches))
for number, value in choices:
print('{:>5}: {:s}'.format(number, value))
choice = input('Which one? ')
try:
choice = choices[int(choice)][1]
except:
print('Don\'t know what to do with "{:s}".'.format(choice))
continue
return self.set(choice)
return self.set(matches[0])
def describe(self):
return 'Directory with the extracted {} source.'.format(self.project)
class BinutilsSourceDir(SourceDirSetting):
key = 'BINUTILS_SRC_DIR'
default = None
pattern = 'binutils-*'
project = 'binutils'
def add_to_argparser(self, parser):
parser.add_argument('--binutils-src', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.binutils_src)
class GccSourceDir(SourceDirSetting):
key = 'GCC_SRC_DIR'
default = None
pattern = 'gcc-*'
project = 'gcc'
def add_to_argparser(self, parser):
parser.add_argument('--gcc-src', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.gcc_src)
class GlibcSourceDir(SourceDirSetting):
key = 'GLIBC_SRC_DIR'
default = None
pattern = 'glibc-*'
project = 'glibc'
def add_to_argparser(self, parser):
parser.add_argument('--glibc-src', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.glibc_src)
class LinuxSourceDir(SourceDirSetting):
key = 'LINUX_SRC_DIR'
default = None
pattern = 'linux-*'
project = 'linux'
def add_to_argparser(self, parser):
parser.add_argument('--linux-src', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.linux_src)
class GdbSourceDir(SourceDirSetting):
key = 'GDB_SRC_DIR'
default = None
pattern = 'gdb-*'
project = 'gdb'
def add_to_argparser(self, parser):
parser.add_argument('--gdb-src', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.gdb_src)
class Parallelism(Setting):
key = 'J'
default = None
def set(self, value):
try:
value = int(value)
except:
print('Can\'t convert "{:s}" into an integer.'.format(value))
if value < 0:
print('Parallelism can\'t be negative.')
return False
self.value = value
self.valid = True
return self.valid
def set_default(self):
self.set(multiprocessing.cpu_count())
def describe(self):
return 'The level of parellism to request from "make".'
def add_to_argparser(self, parser):
parser.add_argument('-j', help=self.describe())
def set_from_args(self, args):
return self.set_arg(args.j)
#
# Steps of the build process.
#
class MetaStep(type):
def __new__(mcls, name, bases, d):
cls = super(MetaStep, mcls).__new__(mcls, name, bases, d)
number = d.get('number', None)
if number is not None:
all_steps[number] = cls()
return cls
@six.add_metaclass(MetaStep)
@six.add_metaclass(abc.ABCMeta)
class Step(object):
'Steps to set up a cross compiling gcc.'
number = None
@abc.abstractmethod
def run(self):
'Execute this step.'
pass
@abc.abstractmethod
def describe(self):
'Return a string describing this step.'
return ''
class Configure(Step):
number = 0
def describe(self):
return 'Adjust settings.'
def get_setting(self):
settings = list(enumerate(all_settings.items()))
all_keys = list(all_settings.keys())
max_key_length = max([len(key) for key in all_keys])
while True:
for number, (key, setting) in settings:
print('{}{:>4}: {:{key_len}s} - {:s}'.format(
' ' if setting.valid else 'X',
number, key, setting.describe(), key_len=max_key_length))
print(' {}'.format(setting.value))
print()
key = input('Value to modify, or "done": ')
if key == "done":
save_settings()
return None
if key not in all_keys:
try:
key = settings[int(key)][1][0]
except:
print('Don\'t know what to do with "{:s}."'.format(key))
continue
return all_settings[key]
def run(self):
while True:
setting = self.get_setting()
if not setting:
return True
new_value = input('New value ({:s}): '.format(setting.get()))
if new_value:
setting.set(new_value)
save_settings()
print_settings()
return True
class BuildBinutils(Step):
number = 1
def describe(self):
return 'Build binutils.'
def run(self):
prefix = Prefix.setting()
target = Target.setting()
j = Parallelism.setting()
source_dir = BinutilsSourceDir.setting()
build_dir = setup_build_dir('binutils')
if not all((prefix, target, j, source_dir, build_dir)):
return False
prefix = prefix.get()
target = target.get()
j = j.get()
build_dir = os.path.abspath(build_dir)
source_dir = os.path.abspath(source_dir.get())
return run_commands(build_dir,
'{configure} --prefix={prefix} --target={target} '
'--disable-multilib'.format(
configure=os.path.join(source_dir, 'configure'),
prefix=prefix, target=target),
'make -j{j}'.format(j=j),
'make install'
)
class InstallLinuxHeaders(Step):
number = 2
def describe(self):
return 'Install Linux headers.'
def run(self):
source_dir = LinuxSourceDir.setting()
linux_arch = LinuxArch.setting()
prefix = Prefix.setting()
target = Target.setting()
if not all((source_dir, linux_arch, prefix, target)):
return False
source_dir = os.path.abspath(source_dir.get())
linux_arch = linux_arch.get()
prefix = os.path.abspath(prefix.get())
target = target.get()
hdr_path = os.path.join(prefix, target)
return run_commands(source_dir,
'make ARCH={arch} INSTALL_HDR_PATH={hdr_path} '
'headers_install'.format(arch=linux_arch, hdr_path=hdr_path))
class Compilers(Step):
number = 3
def describe(self):
return 'Build C and C++ compilers.'
def run(self):
prefix = Prefix.setting()
target = Target.setting()
j = Parallelism.setting()
source_dir = GccSourceDir.setting()
build_dir = setup_build_dir('gcc')
if not all((prefix, target, j, source_dir, build_dir)):
return False
prefix = prefix.get()
target = target.get()
j = j.get()
build_dir = os.path.abspath(build_dir)
source_dir = os.path.abspath(source_dir.get())
return run_commands(build_dir,
'{configure} --prefix={prefix} --target={target} '
'--enable-languages=c,c++ --disable-multilib'.format(
configure=os.path.join(source_dir, 'configure'),
prefix=prefix, target=target),
'make -j{j} all-gcc LIMITS_H_TEST=true'.format(j=j),
'make install-gcc'
)
class CHeaders(Step):
number = 4
def describe(self):
return 'Standard C library headers and startup files.'
def run(self):
prefix = Prefix.setting()
target = Target.setting()
j = Parallelism.setting()
source_dir = GlibcSourceDir.setting()
build_dir = setup_build_dir('glibc')
if not all((prefix, target, j, source_dir, build_dir)):
return False
prefix = prefix.get()
target = target.get()
j = j.get()
source_dir = os.path.abspath(source_dir.get())
build_dir = os.path.abspath(build_dir)
return run_commands(build_dir,
'{configure} --prefix={prefix} --build=$MACHTYPE '
'--host={host} --target={target} --with-headers={hdr_path} '
'--disable-multilib libc_cv_forced_unwind=yes'.format(
configure=os.path.join(source_dir, 'configure'),
prefix=os.path.join(prefix, target),
host=target, target=target,
hdr_path=os.path.join(prefix, target, 'include')),
'make install-bootstrap-headers=yes install-headers',
'make -j{j} csu/subdir_lib'.format(j=j),
'install csu/crt1.o csu/crti.o csu/crtn.o {lib_path}'.format(
lib_path=os.path.join(prefix, target, 'lib')),
'{target}-gcc -nostdlib -nostartfiles -shared -x c /dev/null '
'-o {libc_so}'.format(target=target,
libc_so=os.path.join(prefix, target, 'lib', 'libc.so')),
'touch {stubs_h}'.format(stubs_h=os.path.join(
prefix, target, 'include', 'gnu', 'stubs.h'))
)
class CompilerSupportLib(Step):
number = 5
def describe(self):
return 'Build the compiler support library.'
def run(self):
j = Parallelism.setting()
build_dir = setup_build_dir('gcc')
if not all((j, build_dir)):
return False
j = j.get()
build_dir = os.path.abspath(build_dir)
return run_commands(build_dir,
'make -j{j} all-target-libgcc'.format(j=j),
'make install-target-libgcc'
)
class StandardCLib(Step):
number = 6
def describe(self):
return 'Install the standard C library.'
def run(self):
j = Parallelism.setting()
build_dir = setup_build_dir('glibc')
if not all((j, build_dir)):
return False
j = j.get()
build_dir = os.path.abspath(build_dir)
return run_commands(build_dir,
'make -j{j}'.format(j=j),
'make install',
)
class BuildGdb(Step):
number = 7
def describe(self):
return 'Build GDB.'
def run(self):
prefix = Prefix.setting()
target = Target.setting()
j = Parallelism.setting()
source_dir = GdbSourceDir.setting()
build_dir = setup_build_dir('gdb')
if not all((prefix, target, j, source_dir, build_dir)):
return False
prefix = prefix.get()
target = target.get()
j = j.get()
source_dir = os.path.abspath(source_dir.get())
build_dir = os.path.abspath(build_dir)
return run_commands(build_dir,
'{configure} --prefix={prefix} --target={target} '
'$MACHTYPE'.format(prefix=prefix, target=target,
configure=os.path.join(source_dir, 'configure')),
'make -j{j}'.format(j=j),
'make install'
)
class StandardCxxLib(Step):
number = 8
def describe(self):
return 'Install the standard C++ library.'
def run(self):
j = Parallelism.setting()
build_dir = setup_build_dir('gcc')
if not all((j, build_dir)):
return False
j = j.get()
build_dir = os.path.abspath(build_dir)
return run_commands(build_dir,
'make -j{j}'.format(j=j),
'make install'
)
#
# The engine that makes it all go.
#
def get_steps():
while True:
print()
print('Steps:')
for _, step in sorted(all_steps.items()):
print('{:>5} {:s}'.format(
'{:d}:'.format(step.number), step.describe()))
print()
steps = input('Comma separated list of steps, or '
'"exit", or "all" (all): ')
if not steps:
steps = 'all'
if steps == 'exit':
return []
if steps == 'all':
keys = list([str(key) for key in all_steps.keys()])
steps = ','.join(keys)
try:
return list([all_steps[int(i)] for i in steps.split(",")])
except:
print('Don\'t know what to do with "{:s}"'.format(steps))
def print_settings():
print()
print('Settings:')
for setting in all_settings.values():
print('{} {} = {}'.format(
' ' if setting.valid else 'X', setting.key, setting.value))
def save_settings():
settings = {}
for setting in all_settings.values():
if setting.valid:
settings[setting.key] = setting.get()
with open(SETTINGS_FILE, 'wb') as settings_file:
pickle.dump(settings, settings_file)
def load_settings():
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, 'rb') as settings_file:
settings = pickle.load(settings_file)
else:
settings = {}
for setting in all_settings.values():
if setting.key in settings:
setting.set(settings[setting.key])
def load_settings_file(path):
with open(path, 'r') as settings:
for line in settings.readlines():
if not line:
continue
try:
key, val = line.split('=')
except:
print('Malformated line "{}" in settings file "{}".'.format(
line, path))
return False
key = key.strip()
val = val.strip()
if key not in all_settings:
print('Unknown setting "{}" found in settings '
'file "{}".'.format(key, path))
return False
setting = all_settings[key]
if not setting.set(val):
print('Failed to set "{}" to "{}" from '
'settings file "{}".'.format(key, val, path))
return False
return True
argparser.add_argument('--settings-file',
help='A file with name=value settings to load.')
def main():
# Install command line options for each setting.
for setting in all_settings.values():
setting.add_to_argparser(argparser)
args = argparser.parse_args()
# Load settings from the last time we ran. Lowest priority.
load_settings()
# If requested, read in a settings file. Medium priority.
if args.settings_file:
if not load_settings_file(args.settings_file):
return
# Set settings based on command line options. Highest priority.
for setting in all_settings.values():
setting.set_from_args(args)
# If a setting is still not valid, try setting it to its default.
for setting in all_settings.values():
if not setting.valid:
setting.set_default()
# Print out the resulting settings.
print_settings()
while True:
steps = get_steps()
if not steps:
return
for step in steps:
print()
print('Step {:d}: {:s}'.format(step.number, step.describe()))
print()
if not step.run():
print()
print('Step failed, aborting.')
break
if __name__ == "__main__":
main()