mirror of
				https://github.com/vyos/vyos-build.git
				synced 2025-10-01 20:28:40 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			744 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			744 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| #
 | |
| # Copyright (C) 2022-2024 VyOS maintainers and contributors
 | |
| #
 | |
| # This program is free software; you can redistribute it and/or modify
 | |
| # it under the terms of the GNU General Public License version 2 or later as
 | |
| # published by the Free Software Foundation.
 | |
| #
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU General Public License
 | |
| # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
| #
 | |
| # File: build-vyos-image
 | |
| # Purpose: builds VyOS images using a fork of Debian's live-build tool
 | |
| 
 | |
| # Import Python's standard library modules
 | |
| import re
 | |
| import os
 | |
| import sys
 | |
| import copy
 | |
| import uuid
 | |
| import glob
 | |
| import json
 | |
| import base64
 | |
| import shutil
 | |
| import argparse
 | |
| import datetime
 | |
| import functools
 | |
| import string
 | |
| 
 | |
| ## Check if the script is running wirh root permissions
 | |
| ## Since live-build requires privileged calls such as chroot(),
 | |
| ## there's no real way around it.
 | |
| if os.getuid() != 0:
 | |
|     print("E: this script requires root privileges")
 | |
|     sys.exit(1)
 | |
| 
 | |
| # Import third-party modules
 | |
| try:
 | |
|     import tomli
 | |
|     import jinja2
 | |
|     import git
 | |
| except ModuleNotFoundError as e:
 | |
|     print(f"E: Cannot load required library {e}")
 | |
|     print("E: Please make sure the following Python3 modules are installed: tomli jinja2 git")
 | |
|     sys.exit(1)
 | |
| 
 | |
| # Import local defaults
 | |
| import defaults
 | |
| 
 | |
| ## Load the file with default build configuration options
 | |
| try:
 | |
|     with open(defaults.DEFAULTS_FILE, 'rb') as f:
 | |
|         build_defaults = tomli.load(f)
 | |
| except Exception as e:
 | |
|     print("E: Failed to open the defaults file {0}: {1}".format(defaults.DEFAULTS_FILE, e))
 | |
|     sys.exit(1)
 | |
| 
 | |
| # Checkout vyos-1x under build directory
 | |
| try:
 | |
|     branch_name = build_defaults['vyos_branch']
 | |
|     url_vyos_1x = os.getenv('VYOS1X_REPO_URL', default='https://github.com/vyos/vyos-1x')
 | |
|     path_vyos_1x = os.path.join(defaults.BUILD_DIR, 'vyos-1x')
 | |
|     try:
 | |
|         repo_vyos_1x = git.Repo.clone_from(url_vyos_1x, path_vyos_1x, no_checkout=True)
 | |
|     except git.GitCommandError:
 | |
|         if os.path.exists(path_vyos_1x):
 | |
|             try:
 | |
|                 repo_vyos_1x = git.Repo(path_vyos_1x)
 | |
|             except git.GitError:
 | |
|                 print(f'E: Corrupted vyos-1x git repo: {path_vyos_1x}; remove')
 | |
|                 sys.exit(1)
 | |
|         else:
 | |
|             raise
 | |
|     # alternatively, pass commit hash or tag as arg:
 | |
|     repo_vyos_1x.git.checkout(branch_name)
 | |
| except Exception as e:
 | |
|     print(f'E: Could not retrieve vyos-1x from branch {branch_name}: {repr(e)}')
 | |
|     sys.exit(1)
 | |
| 
 | |
| # Add the vyos-1x directory to the Python path so that
 | |
| # we can import modules from it.
 | |
| VYOS1X_DIR = os.path.join(os.getcwd(), defaults.BUILD_DIR, 'vyos-1x/python')
 | |
| if not os.path.exists(VYOS1X_DIR):
 | |
|     print("E: vyos-1x subdirectory does not exist, did git checkout fail?")
 | |
|     sys.exit(1)
 | |
| else:
 | |
|     sys.path.append(VYOS1X_DIR)
 | |
| 
 | |
| # Import local modules from scripts/image-build
 | |
| # They rely on modules from vyos-1x
 | |
| import utils
 | |
| import raw_image
 | |
| 
 | |
| from utils import cmd, rc_cmd
 | |
| 
 | |
| ## Check if there are missing build dependencies
 | |
| deps = {
 | |
|     'packages': [
 | |
|        'sudo',
 | |
|        'make',
 | |
|        'live-build',
 | |
|        'pbuilder',
 | |
|        'devscripts',
 | |
|        'python3-pystache',
 | |
|        'python3-git',
 | |
|        'qemu-utils',
 | |
|        'gdisk',
 | |
|        'kpartx',
 | |
|        'dosfstools'
 | |
|     ],
 | |
|    'binaries': []
 | |
| }
 | |
| 
 | |
| print("I: Checking if packages required for VyOS image build are installed")
 | |
| try:
 | |
|     utils.check_system_dependencies(deps)
 | |
| except OSError as e:
 | |
|     print(f"E: {e}")
 | |
|     sys.exit(1)
 | |
| 
 | |
| # argparse converts hyphens to underscores,
 | |
| # so for lookups in the original options hash we have to convert them back
 | |
| def field_to_option(s):
 | |
|     return re.sub(r'_', '-', s)
 | |
| 
 | |
| def get_validator(optdict, name):
 | |
|     try:
 | |
|         return optdict[name][1]
 | |
|     except KeyError:
 | |
|         return None
 | |
| 
 | |
| def merge_defaults(source, defaults={}, skip_none=False):
 | |
|     """ Merge a dict with values from a defaults dict.
 | |
|     Merging logic is as follows:
 | |
|       Sub-dicts are merged.
 | |
|       List values are combined.
 | |
|       Scalar values are set to those from the source dict,
 | |
|       if they exist there.
 | |
|     """
 | |
|     from copy import deepcopy
 | |
|     tmp = deepcopy(defaults)
 | |
| 
 | |
|     for key, value in source.items():
 | |
|         if key not in tmp:
 | |
|             tmp[key] = value
 | |
|         elif isinstance(source[key], dict):
 | |
|             tmp[key] = merge_defaults(source[key], tmp[key])
 | |
|         elif isinstance(source[key], list):
 | |
|             tmp[key] = source[key] + tmp[key]
 | |
|         elif not skip_none or source[key] is not None:
 | |
|             tmp[key] = source[key]
 | |
| 
 | |
|     return tmp
 | |
| 
 | |
| def has_nonempty_key(config, key):
 | |
|     if key in config:
 | |
|         if config[key]:
 | |
|             return True
 | |
|     return False
 | |
| 
 | |
| def make_toml_path(dir, file_basename):
 | |
|     return os.path.join(dir, file_basename + ".toml")
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     ## Get a list of available build flavors
 | |
|     flavor_dir_env = os.getenv("VYOS_BUILD_FLAVORS_DIR")
 | |
|     if flavor_dir_env:
 | |
|         flavor_dir = flavor_dir_env
 | |
|     else:
 | |
|         flavor_dir = defaults.BUILD_FLAVORS_DIR
 | |
| 
 | |
|     print(f"I: using build flavors directory {flavor_dir}")
 | |
|     build_flavors = [f[0] for f in map(os.path.splitext, os.listdir(flavor_dir)) if (f[1] == ".toml")]
 | |
| 
 | |
|     ## Set up the option parser
 | |
|     ## XXX: It uses values from the default configuration file for its option defaults,
 | |
|     ## which is why it's defined after loading the defaults.toml file data.
 | |
| 
 | |
|     # Options dict format:
 | |
|     # '$option_name_without_leading_dashes': { ('$help_string', $default_value_generator_thunk, $value_checker_thunk) }
 | |
|     options = {
 | |
|        'architecture': ('Image target architecture (amd64 or arm64)', lambda x: x in ['amd64', 'arm64', None]),
 | |
|        'build-by': ('Builder identifier (e.g. jrandomhacker@example.net)', None),
 | |
|        'debian-mirror': ('Debian repository mirror', None),
 | |
|        'debian-security-mirror': ('Debian security updates mirror', None),
 | |
|        'pbuilder-debian-mirror': ('Debian repository mirror for pbuilder env bootstrap', None),
 | |
|        'vyos-mirror': ('VyOS package mirror', None),
 | |
|        'build-type': ('Build type, release or development', lambda x: x in ['release', 'development']),
 | |
|        'version': ('Version string', None),
 | |
|        'build-comment': ('Optional build comment', None),
 | |
|        'build-hook-opts': ('Custom options for the post-build hook', None),
 | |
|        'bootloaders': ('Bootloaders to include in the image', None)
 | |
|     }
 | |
| 
 | |
|     # Create the option parser
 | |
|     parser = argparse.ArgumentParser()
 | |
|     for k, v in options.items():
 | |
|         help_string = v[0]
 | |
|         parser.add_argument('--' + k, type=str, help=help_string)
 | |
| 
 | |
|     # Debug options
 | |
|     parser.add_argument('--debug', help='Enable debug output', action='store_true')
 | |
|     parser.add_argument('--dry-run', help='Check build configuration and exit', action='store_true')
 | |
| 
 | |
|     # Custom APT entry and APT key options can be used multiple times
 | |
|     parser.add_argument('--custom-apt-entry', help="Custom APT entry", action='append')
 | |
|     parser.add_argument('--custom-apt-key', help="Custom APT key file", action='append')
 | |
|     parser.add_argument('--custom-package', help="Custom package to install from repositories", action='append')
 | |
| 
 | |
|     # Options relevant for non-ISO format flavors
 | |
|     parser.add_argument('--reuse-iso', help='Use an existing ISO file to build additional image formats', type=str, action='store')
 | |
|     parser.add_argument('--disk-size', help='Disk size for non-ISO image formats', type=int, action='store')
 | |
| 
 | |
|     # Build flavor is a positional argument
 | |
|     parser.add_argument('build_flavor', help='Build flavor', nargs='?', action='store')
 | |
| 
 | |
|     args = vars(parser.parse_args())
 | |
| 
 | |
|     debug = args["debug"]
 | |
| 
 | |
|     # Validate options
 | |
|     for k, v in args.items():
 | |
|         key = field_to_option(k)
 | |
|         func = get_validator(options, k)
 | |
|         if func is not None:
 | |
|             if not func(v):
 | |
|                 print("E: {v} is not a valid value for --{o} option".format(o=key, v=v))
 | |
|                 sys.exit(1)
 | |
| 
 | |
|     if not args["build_flavor"]:
 | |
|         print("E: Build flavor is not specified!")
 | |
|         print("E: For example, to build the generic ISO, run {} iso".format(sys.argv[0]))
 | |
|         print("Available build flavors:\n")
 | |
|         print("\n".join(build_flavors))
 | |
|         sys.exit(1)
 | |
| 
 | |
|     ## Try to get correct architecture and build type from build flavor and CLI arguments
 | |
|     pre_build_config = copy.deepcopy(build_defaults)
 | |
| 
 | |
|     flavor_config = {}
 | |
|     build_flavor = args["build_flavor"]
 | |
|     try:
 | |
|         toml_flavor_file = make_toml_path(flavor_dir, args["build_flavor"])
 | |
|         with open(toml_flavor_file, 'rb') as f:
 | |
|             flavor_config = tomli.load(f)
 | |
|             pre_build_config = merge_defaults(flavor_config, defaults=pre_build_config)
 | |
|     except FileNotFoundError:
 | |
|         print(f"E: Flavor '{build_flavor}' does not exist")
 | |
|         sys.exit(1)
 | |
|     except tomli.TOMLDecodeError as e:
 | |
|         print(f"E: Failed to parse TOML file for flavor '{build_flavor}': {e}")
 | |
|         sys.exit(1)
 | |
| 
 | |
|     ## Combine configs args > flavor > defaults
 | |
|     pre_build_config = merge_defaults(args, defaults=pre_build_config, skip_none=True)
 | |
| 
 | |
|     # Some fixup for mirror settings.
 | |
|     # The idea is: if --debian-mirror is specified
 | |
|     # but --pbuilder-debian-mirror or --debian-security-mirror are not,
 | |
|     # use the --debian-mirror value for those
 | |
|     if pre_build_config['debian_mirror'] is None:
 | |
|         print("E: debian_mirror must be specified")
 | |
|         sys.exit(1)
 | |
| 
 | |
|     if pre_build_config['pbuilder_debian_mirror'] is None:
 | |
|         pre_build_config['pbuilder_debian_mirror'] = pre_build_config['debian_mirror']
 | |
| 
 | |
|     if pre_build_config['debian_security_mirror'] is None:
 | |
|         pre_build_config['debian_security_mirror'] = pre_build_config['debian_mirror']
 | |
| 
 | |
|     # Validate characters in version name
 | |
|     if args.get('version'):
 | |
|         allowed = string.ascii_letters + string.digits + '.' + '-' + '+'
 | |
|         if not set(args['version']) <= set(allowed):
 | |
|             print(f'Version string contains illegal character(s), allowed: {allowed}')
 | |
|             sys.exit(1)
 | |
| 
 | |
|     ## Inject some useful hardcoded options
 | |
|     args['build_dir'] = defaults.BUILD_DIR
 | |
|     args['pbuilder_config'] = os.path.join(defaults.BUILD_DIR, defaults.PBUILDER_CONFIG)
 | |
| 
 | |
|     ## Add hardcoded defaults
 | |
|     build_config = merge_defaults(defaults.HARDCODED_BUILD, defaults={})
 | |
| 
 | |
|     ## Combine the arguments with non-configurable defaults
 | |
|     build_config = merge_defaults(build_defaults, defaults=build_config)
 | |
| 
 | |
|     ## Load correct mix-ins
 | |
|     with open(make_toml_path(defaults.BUILD_TYPES_DIR, pre_build_config["build_type"]), 'rb') as f:
 | |
|         build_type_config = tomli.load(f)
 | |
|         build_config = merge_defaults(build_type_config, defaults=build_config)
 | |
| 
 | |
|     with open(make_toml_path(defaults.BUILD_ARCHES_DIR, pre_build_config["architecture"]), 'rb') as f:
 | |
|         build_arch_config = tomli.load(f)
 | |
|         build_config = merge_defaults(build_arch_config, defaults=build_config)
 | |
| 
 | |
|     ## Override with flavor and then CLI arguments
 | |
|     build_config = merge_defaults(flavor_config, defaults=build_config)
 | |
|     build_config = merge_defaults(args, defaults=build_config, skip_none=True)
 | |
| 
 | |
|     # If Debian mirror is specified explicitly but Debian security mirror is not,
 | |
|     # assume that the user wants to use that mirror for security updates as well.
 | |
|     if (args['debian_mirror'] is not None) and (args['debian_security_mirror'] is None):
 | |
|         build_config['debian_security_mirror'] = args['debian_mirror']
 | |
| 
 | |
|     ## Rename and merge some fields for simplicity
 | |
|     ## E.g. --custom-packages is for the user, but internally
 | |
|     ## it's added to the same package list as everything else
 | |
|     if has_nonempty_key(build_config, "custom_package"):
 | |
|         build_config["packages"] += build_config["custom_package"]
 | |
|         del build_config["custom_package"]
 | |
| 
 | |
|     ## Add architecture-dependent packages from the flavor
 | |
|     if has_nonempty_key(build_config, "architectures"):
 | |
|         arch = build_config["architecture"]
 | |
|         if arch in build_config["architectures"]:
 | |
|             if has_nonempty_key(build_config["architectures"][arch], "packages"):
 | |
|                 build_config["packages"] += build_config["architectures"][arch]["packages"]
 | |
| 
 | |
|     ## Check if image format is specified,
 | |
|     ## else we have no idea what we are actually supposed to build.
 | |
|     if not has_nonempty_key(build_config, "image_format"):
 | |
|         print("E: image format is not specified in the build flavor file")
 | |
|         sys.exit(1)
 | |
| 
 | |
|     ## Override bootloaders if specified
 | |
|     if args['bootloaders'] is not None:
 | |
|         build_config['bootloaders'] = args['bootloaders']
 | |
| 
 | |
|     ## Add default boot settings if needed
 | |
|     if "boot_settings" not in build_config:
 | |
|         build_config["boot_settings"] = defaults.boot_settings
 | |
|     else:
 | |
|         build_config["boot_settings"] = merge_defaults(build_config["boot_settings"], defaults=defaults.boot_settings)
 | |
| 
 | |
|     ## Convert the image_format field to a single-item list if it's a scalar
 | |
|     ## (like `image_format = "iso"`)
 | |
|     if type(build_config["image_format"]) != list:
 | |
|         build_config["image_format"] = [ build_config["image_format"] ]
 | |
| 
 | |
|     ## If the user didn't explicitly specify what extensions build artifact should have,
 | |
|     ## assume that the list is the same as image formats.
 | |
|     ## One case when it's not the same is when a custom build hook is used
 | |
|     ## to build a format that our build script doesn't support natively.
 | |
|     if not has_nonempty_key(build_config, "artifact_format"):
 | |
|         build_config["artifact_format"] = build_config["image_format"]
 | |
|     else:
 | |
|         # If the option is there, also make it list if it's a scalar
 | |
|         if type(build_config["artifact_format"]) != list:
 | |
|             build_config["artifact_format"] = [ build_config["artifact_format"] ]
 | |
| 
 | |
|     ## Dump the complete config if the user enabled debug mode
 | |
|     if debug:
 | |
|         import json
 | |
|         print("D: Effective build config:\n")
 | |
|         print(json.dumps(build_config, indent=4))
 | |
| 
 | |
|     ## Clean up the old build config and set up a fresh copy
 | |
|     lb_config_dir = os.path.join(defaults.BUILD_DIR, defaults.LB_CONFIG_DIR)
 | |
|     shutil.rmtree(lb_config_dir, ignore_errors=True)
 | |
|     shutil.copytree("data/live-build-config/", lb_config_dir)
 | |
|     os.makedirs(lb_config_dir, exist_ok=True)
 | |
| 
 | |
|     # Switch to the build directory, this is crucial for the live-build work
 | |
|     # because the efective build config files etc. are there.
 | |
|     #
 | |
|     # All directory paths from this point must be relative to BUILD_DIR,
 | |
|     # not to the vyos-build repository root.
 | |
|     os.chdir(defaults.BUILD_DIR)
 | |
| 
 | |
|     ## Initialize build manifest
 | |
|     manifest = {
 | |
|       'build_config' : build_config,
 | |
|       'artifacts' : [],
 | |
|       'pre_build_config' : pre_build_config
 | |
|     }
 | |
| 
 | |
|     iso_file = None
 | |
| 
 | |
|     if build_config["reuse_iso"]:
 | |
|         iso_file = build_config["reuse_iso"]
 | |
|         manifest["artifacts"] = [iso_file]
 | |
|     else:
 | |
|         ## Create the version file
 | |
| 
 | |
|         # Create a build timestamp
 | |
|         now = datetime.datetime.today()
 | |
|         build_timestamp = now.strftime("%Y%m%d%H%M")
 | |
| 
 | |
|         # FIXME: use aware rather than naive object
 | |
|         build_date = now.strftime("%a %d %b %Y %H:%M UTC")
 | |
| 
 | |
|         # Assign a (hopefully) unique identifier to the build (UUID)
 | |
|         build_uuid = str(uuid.uuid4())
 | |
| 
 | |
|         # Initialize Git object from our repository
 | |
|         try:
 | |
|             repo = git.Repo('.', search_parent_directories=True)
 | |
|             # Retrieve the Git commit ID of the repository, 14 charaters will be sufficient
 | |
|             build_git = repo.head.object.hexsha[:14]
 | |
|             # If somone played around with the source tree and the build is "dirty", mark it
 | |
|             if repo.is_dirty():
 | |
|                 build_git += "-dirty"
 | |
| 
 | |
|             # Retrieve git branch name or current tag
 | |
|             # Building a tagged release might leave us checking out a git tag that is not the tip of a named branch (detached HEAD)
 | |
|             # Check if the current HEAD is associated with a tag and use its name instead of an unavailable branch name.
 | |
|             git_branch = next((tag.name for tag in repo.tags if tag.commit == repo.head.commit), None)
 | |
|             if git_branch is None:
 | |
|                 git_branch = repo.active_branch.name
 | |
|         except Exception as e:
 | |
|             print(f'W: Could not retrieve information from git: {repr(e)}')
 | |
|             build_git = ""
 | |
|             git_branch = ""
 | |
| 
 | |
|         # Create the build version string, if it's not explicitly given
 | |
|         if build_config.get('version'):
 | |
|             version = build_config['version']
 | |
|         else:
 | |
|             try:
 | |
|                 if not git_branch:
 | |
|                     raise ValueError("git branch could not be determined")
 | |
| 
 | |
|                 # Load the branch to version mapping file
 | |
|                 with open('../data/versions') as f:
 | |
|                     version_mapping = json.load(f)
 | |
| 
 | |
|                 branch_version = version_mapping[git_branch]
 | |
| 
 | |
|                 version = "{0}-rolling-{1}".format(branch_version, build_timestamp)
 | |
|             except Exception as e:
 | |
|                 print("W: Could not build a version string specific to git branch, falling back to default: {0}".format(str(e)))
 | |
|                 version = "999.{0}".format(build_timestamp)
 | |
| 
 | |
|             build_config['version'] = version
 | |
| 
 | |
|         version_data = {
 | |
|             'version': version,
 | |
|             'flavor': build_config["build_flavor"],
 | |
|             'built_by': build_config['build_by'],
 | |
|             'built_on': build_date,
 | |
|             'build_uuid': build_uuid,
 | |
|             'build_git': build_git,
 | |
|             'build_branch': git_branch,
 | |
|             'release_train': build_config['release_train'],
 | |
|             'architecture': build_config['architecture'],
 | |
|             'build_type': build_config['build_type'],
 | |
|             'build_comment': build_config['build_comment'],
 | |
|             'bugtracker_url': build_config['bugtracker_url'],
 | |
|             'documentation_url': build_config['documentation_url'],
 | |
|             'project_news_url': build_config['project_news_url'],
 | |
|             'support_url': build_config['support_url']
 | |
|         }
 | |
| 
 | |
|         # Multi line strings needs to be un-indented to not have leading
 | |
|         # whitespaces in the resulting file
 | |
|         os_release = f"""PRETTY_NAME="VyOS {version} ({build_config['release_train']})"
 | |
| NAME="VyOS"
 | |
| VERSION_ID="{version}"
 | |
| VERSION="{version} ({build_config['release_train']})"
 | |
| VERSION_CODENAME={build_defaults['release_train']}
 | |
| ID=vyos
 | |
| BUILD_ID="{build_git}"
 | |
| HOME_URL="{build_defaults['website_url']}"
 | |
| SUPPORT_URL="{build_defaults['support_url']}"
 | |
| BUG_REPORT_URL="{build_defaults['bugtracker_url']}"
 | |
| DOCUMENTATION_URL="{build_config['documentation_url']}"
 | |
| """
 | |
| 
 | |
|         # Reminder: all paths relative to the build dir, not to the repository root
 | |
|         chroot_includes_dir = defaults.CHROOT_INCLUDES_DIR
 | |
|         binary_includes_dir = defaults.BINARY_INCLUDES_DIR
 | |
|         vyos_data_dir = os.path.join(chroot_includes_dir, "usr/share/vyos")
 | |
|         os.makedirs(vyos_data_dir, exist_ok=True)
 | |
|         with open(os.path.join(vyos_data_dir, 'version.json'), 'w') as f:
 | |
|             json.dump(version_data, f)
 | |
|         with open(os.path.join(binary_includes_dir, 'version.json'), 'w') as f:
 | |
|             json.dump(version_data, f)
 | |
| 
 | |
|         # For backwards compatibility with 'add system image' script from older versions
 | |
|         # we need a file in the old format so that script can find out the version of the image
 | |
|         # for upgrade
 | |
|         os.makedirs(os.path.join(chroot_includes_dir, 'opt/vyatta/etc/'), exist_ok=True)
 | |
|         with open(os.path.join(chroot_includes_dir, 'opt/vyatta/etc/version'), 'w') as f:
 | |
|             print("Version: {0}".format(version), file=f)
 | |
| 
 | |
|         # Define variables that influence to welcome message on boot
 | |
|         os.makedirs(os.path.join(chroot_includes_dir, 'etc/'), exist_ok=True)
 | |
|         with open(os.path.join(chroot_includes_dir, 'etc/os-release'), 'w') as f:
 | |
|             print(os_release, file=f)
 | |
| 
 | |
|         ## Clean up earlier build state and artifacts
 | |
|         print("I: Cleaning the build workspace")
 | |
|         cmd("lb clean")
 | |
|         #iter(lambda p: shutil.rmtree(p, ignore_errors=True),
 | |
|         #  ['config/binary', 'config/bootstrap', 'config/chroot', 'config/common', 'config/source'])
 | |
|         artifacts = functools.reduce(
 | |
|           lambda x, y: x + y,
 | |
|           map(glob.glob, ['*.iso', '*.raw', '*.img', '*.xz', '*.ova', '*.ovf']))
 | |
|         iter(os.remove, artifacts)
 | |
| 
 | |
|         ## Create the target ISO file path
 | |
|         iso_file = f"vyos-{version_data['version']}-{build_config['build_flavor']}-{build_config['architecture']}.iso"
 | |
| 
 | |
|         ## Create live-build configuration files
 | |
| 
 | |
|         # Add the additional repositories to package lists
 | |
|         print("I: Setting up VyOS repository APT entries")
 | |
|         vyos_repo_entry = "deb {vyos_mirror} {vyos_branch} main\n".format(**build_config)
 | |
|         vyos_repo_entry += "deb-src {vyos_mirror} {vyos_branch} main\n".format(**build_config)
 | |
| 
 | |
|         apt_file = defaults.VYOS_REPO_FILE
 | |
| 
 | |
|         if debug:
 | |
|             print(f"D: Adding these entries to {apt_file}:")
 | |
|             print("\t", vyos_repo_entry)
 | |
| 
 | |
|         with open(apt_file, 'w') as f:
 | |
|             f.write(vyos_repo_entry)
 | |
| 
 | |
|         # Add custom APT entries
 | |
|         print("I: Setting up additional APT entries")
 | |
|         if build_config.get('additional_repositories', False):
 | |
|             for r in build_config['additional_repositories']:
 | |
|                 repo_data = build_config['additional_repositories'][r]
 | |
| 
 | |
|                 url = repo_data.get('url', None)
 | |
|                 arch = repo_data.get('architecture', None)
 | |
|                 distro = repo_data.get('distribution', build_config['debian_distribution'])
 | |
|                 components = repo_data.get('components', 'main')
 | |
| 
 | |
|                 if not url:
 | |
|                     print(f'E: repository {r} does not specify URL')
 | |
|                     sys.exit(1)
 | |
| 
 | |
|                 if arch:
 | |
|                     arch_string = f'[arch={arch}]'
 | |
|                 else:
 | |
|                     arch_string = ''
 | |
| 
 | |
|                 entry = f'deb {arch_string} {url} {distro} {components}'
 | |
|                 build_config['custom_apt_entry'].append(entry)
 | |
| 
 | |
|                 if not repo_data.get('no_source', False):
 | |
|                     src_entry = f'deb-src {url} {distro} {components}'
 | |
|                     build_config['custom_apt_entry'].append(src_entry)
 | |
| 
 | |
|                 if repo_data.get('key', None):
 | |
|                     build_config['custom_apt_keys'].append({'name': r, 'key': repo_data['key']})
 | |
| 
 | |
|         if build_config.get('custom_apt_entry', []):
 | |
|             custom_apt_file = defaults.CUSTOM_REPO_FILE
 | |
|             entries = "\n".join(build_config['custom_apt_entry'])
 | |
|             if debug:
 | |
|                 print("D: Adding custom APT entries:")
 | |
|                 print(entries)
 | |
|             with open(custom_apt_file, 'w') as f:
 | |
|                 f.write(entries)
 | |
|                 f.write("\n")
 | |
| 
 | |
|         # Add custom APT keys
 | |
|         if has_nonempty_key(build_config, 'custom_apt_keys'):
 | |
|             key_dir = defaults.ARCHIVES_DIR
 | |
|             for k in build_config['custom_apt_keys']:
 | |
|                 dst_name = '{0}.key.chroot'.format(k['name'])
 | |
|                 with open(os.path.join(key_dir, dst_name), 'bw') as f:
 | |
|                     key_data = base64.b64decode(k['key'])
 | |
|                     f.write(key_data)
 | |
| 
 | |
|         # Add custom packages
 | |
|         if has_nonempty_key(build_config, 'packages'):
 | |
|             package_list_file = defaults.PACKAGE_LIST_FILE
 | |
|             packages = "\n".join(build_config['packages'])
 | |
|             with open (package_list_file, 'w') as f:
 | |
|                 f.write(packages)
 | |
| 
 | |
|         ## Create includes
 | |
|         if has_nonempty_key(build_config, "includes_chroot"):
 | |
|             for i in build_config["includes_chroot"]:
 | |
|                 file_path = os.path.join(chroot_includes_dir, i["path"])
 | |
|                 if debug:
 | |
|                     print(f"D: Creating chroot include file: {file_path}")
 | |
|                 os.makedirs(os.path.dirname(file_path), exist_ok=True)
 | |
|                 with open(file_path, 'w') as f:
 | |
|                     f.write(i["data"])
 | |
| 
 | |
|         if has_nonempty_key(build_config, "includes_binary"):
 | |
|             for i in build_config["includes_binary"]:
 | |
|                 file_path = os.path.join(binary_includes_dir, i["path"])
 | |
|                 if debug:
 | |
|                     print(f"D: Creating binary image include file: {file_path}")
 | |
|                 os.makedirs(os.path.dirname(file_path), exist_ok=True)
 | |
|                 with open(file_path, 'w') as f:
 | |
|                     f.write(i["data"])
 | |
| 
 | |
|         ## Create the default config
 | |
|         ## Technically it's just another includes.chroot entry,
 | |
|         ## but it's special enough to warrant making it easier for flavor writers
 | |
|         if has_nonempty_key(build_config, "default_config"):
 | |
|             file_path = os.path.join(chroot_includes_dir, "opt/vyatta/etc/config.boot.default")
 | |
|             os.makedirs(os.path.dirname(file_path), exist_ok=True)
 | |
|             with open(file_path, 'w') as f:
 | |
|                 f.write(build_config["default_config"])
 | |
| 
 | |
|         ## Configure live-build
 | |
|         lb_config_tmpl = jinja2.Template("""
 | |
|         lb config noauto \
 | |
|                 --apt-indices false \
 | |
|                 --apt-options "--yes -oAPT::Get::allow-downgrades=true" \
 | |
|                 --apt-recommends false \
 | |
|                 --architecture {{architecture}} \
 | |
|                 --archive-areas {{debian_archive_areas}} \
 | |
|                 --backports true \
 | |
|                 --binary-image iso-hybrid \
 | |
|                 --bootappend-live "boot=live components hostname=vyos username=live nopersistence noautologin nonetworking union=overlay console=ttyS0,115200 console=tty0 net.ifnames=0 biosdevname=0" \
 | |
|                 --bootappend-live-failsafe "live components memtest noapic noapm nodma nomce nolapic nomodeset nosmp nosplash vga=normal console=ttyS0,115200 console=tty0 net.ifnames=0 biosdevname=0" \
 | |
|                 --bootloaders {{bootloaders}} \
 | |
|                 --checksums 'sha256 md5' \
 | |
|                 --chroot-squashfs-compression-type "{{squashfs_compression_type}}" \
 | |
|                 --debian-installer none \
 | |
|                 --debootstrap-options "--variant=minbase --exclude=isc-dhcp-client,isc-dhcp-common,ifupdown --include=apt-utils,ca-certificates,gnupg2,linux-kbuild-6.1" \
 | |
|                 --distribution {{debian_distribution}} \
 | |
|                 --firmware-binary false \
 | |
|                 --firmware-chroot false \
 | |
|                 --iso-application "VyOS" \
 | |
|                 --iso-publisher "{{build_by}}" \
 | |
|                 --iso-volume "VyOS" \
 | |
|                 --linux-flavours {{kernel_flavor}} \
 | |
|                 --linux-packages linux-image-{{kernel_version}} \
 | |
|                 --mirror-binary {{debian_mirror}} \
 | |
|                 --mirror-binary-security {{debian_security_mirror}} \
 | |
|                 --mirror-bootstrap {{debian_mirror}} \
 | |
|                 --mirror-chroot {{debian_mirror}} \
 | |
|                 --mirror-chroot-security {{debian_security_mirror}} \
 | |
|                 --security true \
 | |
|                 --updates true \
 | |
|                 --utc-time true
 | |
|                 "${@}"
 | |
|         """)
 | |
| 
 | |
|         lb_config_command = lb_config_tmpl.render(build_config)
 | |
| 
 | |
|         ## Pin release for VyOS packages
 | |
|         apt_pin = f"""Package: *
 | |
| Pin: release n={build_config['release_train']}
 | |
| Pin-Priority: 600
 | |
| """
 | |
| 
 | |
|         with open(defaults.VYOS_PIN_FILE, 'w') as f:
 | |
|             f.write(apt_pin)
 | |
| 
 | |
|         print("I: Configuring live-build")
 | |
| 
 | |
|         if debug:
 | |
|             print("D: live-build configuration command")
 | |
|             print(lb_config_command)
 | |
| 
 | |
|         cmd(lb_config_command)
 | |
| 
 | |
|         ## In dry-run mode, exit at this point
 | |
|         if build_config["dry_run"]:
 | |
|             print("I: dry-run, not starting image build")
 | |
|             sys.exit(0)
 | |
| 
 | |
|         ## Add local packages
 | |
|         local_packages = glob.glob('../packages/*.deb')
 | |
|         if local_packages:
 | |
|             for f in local_packages:
 | |
|                 shutil.copy(f, os.path.join(defaults.LOCAL_PACKAGES_PATH, os.path.basename(f)))
 | |
| 
 | |
|         ## Build the image
 | |
|         print("I: Starting image build")
 | |
|         if debug:
 | |
|             print("D: It's not like I'm building this specially for you or anything!")
 | |
|         cmd("lb build 2>&1")
 | |
| 
 | |
|         # Copy the image
 | |
|         shutil.copy("live-image-{0}.hybrid.iso".format(build_config["architecture"]), iso_file)
 | |
| 
 | |
|         # Add the image to the manifest
 | |
|         manifest['artifacts'].append(iso_file)
 | |
| 
 | |
|     # If the flavor has `image_format = "iso"`, then the work is done.
 | |
|     # If not, build additional flavors from the ISO.
 | |
|     if build_config["image_format"] != ["iso"]:
 | |
|         # For all non-iso formats, we always build a raw image first
 | |
|         version_data, raw_image = raw_image.create_raw_image(build_config, iso_file, "tmp/")
 | |
|         manifest['artifacts'].append(raw_image)
 | |
| 
 | |
|         # If there are other formats in the flavor, the assumptions is that
 | |
|         # they can be produced from a raw image with `qemu-img convert`
 | |
|         other_formats = filter(lambda x: x not in ["iso", "raw"], build_config["image_format"])
 | |
|         for f in other_formats:
 | |
|             image_ext = build_config.get("image_ext", f)
 | |
|             image_opts = build_config.get("image_opts", "")
 | |
|             target = f"{os.path.splitext(raw_image)[0]}.{image_ext}"
 | |
|             print(f"I: Building {f} file {target}")
 | |
|             cmd(f"qemu-img convert -f raw -O {f} {image_opts} {raw_image} {target}")
 | |
|             manifest['artifacts'].append(target)
 | |
| 
 | |
|         # Finally, there are formats that require special procedures.
 | |
|         # For example, a VMware OVA needs a custom tool for image building and signing.
 | |
|         # For those cases, we support custom build hooks that run on raw images.
 | |
|         # A post-build hook is a script embedded in the flavor file
 | |
|         # that must return the artifact name via stdout.
 | |
|         if has_nonempty_key(build_config, "build_hook"):
 | |
|             hook_source = build_config["build_hook"]
 | |
| 
 | |
|             with open('build_hook', 'w') as f:
 | |
|                 f.write(hook_source)
 | |
|             os.chmod('build_hook', 0o755)
 | |
| 
 | |
|             if has_nonempty_key(build_config, "build_hook_opts"):
 | |
|                 hook_opts = build_config["build_hook_opts"]
 | |
|             else:
 | |
|                 hook_opts = ""
 | |
|             build_hook_command = f"./build_hook {raw_image} {version_data['version']} \
 | |
|               {build_config['architecture']} {hook_opts}"
 | |
|             print(f'I: executing build hook command: {build_hook_command}')
 | |
|             custom_image = rc_cmd(build_hook_command)
 | |
|             manifest['artifacts'].append(custom_image)
 | |
| 
 | |
|     # Filter out unwanted files from the artifact list
 | |
|     # and leave only those the user specified
 | |
|     # in either `artifact_format` or `image_format`.
 | |
|     #
 | |
|     # For example, with `image_format = "raw"`,
 | |
|     # the ISO image is just an intermediate object, not an target artifact.
 | |
| 
 | |
|     # os.path.splitext returns extensions with dots,
 | |
|     # so we need to remove the dots, hence [1:]
 | |
|     is_artifact = lambda f: os.path.splitext(f)[-1][1:] in build_config['artifact_format']
 | |
| 
 | |
|     manifest['artifacts'] = list(filter(is_artifact, manifest['artifacts']))
 | |
| 
 | |
|     with open('manifest.json', 'w') as f:
 | |
|         f.write(json.dumps(manifest))
 |