From c58a3787eca9c0c4a7f376ba841cd7e39ab95ece Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 28 Mar 2020 23:01:09 +0000 Subject: project.boost: factor out everything else I finally snapped. This starts to resemble sensible structure though. --- project/boost/archive.py | 87 ++++++++++++ project/boost/build.py | 346 ++++++++------------------------------------- project/boost/directory.py | 60 ++++++++ project/boost/download.py | 112 +++++++++++++++ 4 files changed, 318 insertions(+), 287 deletions(-) create mode 100644 project/boost/archive.py create mode 100644 project/boost/directory.py create mode 100644 project/boost/download.py (limited to 'project/boost') diff --git a/project/boost/archive.py b/project/boost/archive.py new file mode 100644 index 0000000..55d05f2 --- /dev/null +++ b/project/boost/archive.py @@ -0,0 +1,87 @@ +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +import abc +from contextlib import contextmanager +import logging +import os.path +import shutil +import tempfile + +from project.boost.directory import BoostDir + + +class Archive: + def __init__(self, version, path): + self.version = version + self.path = path + + @property + def dir_name(self): + return self.version.dir_name + + def unpack(self, dest_dir): + path = os.path.join(dest_dir, self.dir_name) + if os.path.exists(path): + raise RuntimeError(f'Boost directory already exists: {path}') + logging.info('Unpacking Boost to: %s', path) + shutil.unpack_archive(self.path, dest_dir) + return BoostDir(path) + + +class ArchiveStorage(abc.ABC): + @abc.abstractmethod + def get_archive(self, version): + pass + + @contextmanager + @abc.abstractmethod + def write_archive(self, version, contents): + pass + + +class PermanentStorage(ArchiveStorage): + def __init__(self, cache_dir): + self._dir = cache_dir + + def _archive_path(self, version): + return os.path.join(self._dir, version.archive_name) + + def get_archive(self, version): + path = self._archive_path(version) + if os.path.exists(path): + return path + return None + + @contextmanager + def write_archive(self, version, contents): + path = self._archive_path(version) + logging.info('Writing Boost archive: %s', path) + if os.path.exists(path): + raise RuntimeError(f'cannot download Boost, file already exists: {path}') + with open(path, mode='w+b') as dest: + dest.write(contents) + yield path + + +class TemporaryStorage(ArchiveStorage): + def __init__(self, temp_dir): + self._dir = temp_dir + + def get_archive(self, version): + return None + + @contextmanager + def write_archive(self, version, contents): + with tempfile.NamedTemporaryFile(prefix=f'boost_{version}_', suffix=version.archive_ext, + dir=self._dir, delete=False) as dest: + path = dest.name + logging.info('Writing Boost archive: %s', path) + dest.write(contents) + try: + yield path + finally: + logging.info('Removing temporary Boost archive: %s', path) + os.remove(path) diff --git a/project/boost/build.py b/project/boost/build.py index e9f2ffc..541e2ba 100644 --- a/project/boost/build.py +++ b/project/boost/build.py @@ -1,240 +1,59 @@ -#!/usr/bin/env python3 - # Copyright (c) 2019 Egor Tensin # This file is part of the "cmake-common" project. # For details, see https://github.com/egor-tensin/cmake-common. # Distributed under the MIT License. -'''Download & build Boost. - -This script downloads and builds the Boost libraries. It's main purpose is to: -1) provide a cross-platform way to download & unpack the Boost distribution -archive, -2) set the correct --stagedir parameter value to avoid name clashes. +R'''Build Boost. -Please pick a command below. You can execute `%(prog)s COMMAND --help` to view -its usage message. +This script builds the Boost libraries. It's main utility is setting the +correct --stagedir parameter value to avoid name clashes. -A simple usage example: +Usage examples: - $ %(prog)s download 1.71.0 - ... - $ %(prog)s build -- boost_1_71_0/ --with-filesystem --with-program_options + $ %(prog)s -- boost_1_71_0/ --with-filesystem --with-program_options ... ''' -import abc import argparse -from collections import namedtuple from contextlib import contextmanager -from enum import Enum -from functools import total_ordering import logging import os.path -import platform -import re -import shutil -import subprocess import sys import tempfile -import urllib.request -from project.boost.version import Version +from project.boost.directory import BoostDir from project.configuration import Configuration from project.linkage import Linkage from project.platform import Platform - - -@contextmanager -def _chdir(path): - cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(cwd) - - -def _setup_logging(): - logging.basicConfig( - format='%(asctime)s | %(levelname)s | %(message)s', - level=logging.INFO) - - -def _on_windows(): - return platform.system() == 'Windows' - - -def _on_linux(): - return not _on_windows() - - -def _run_executable(cmd_line): - logging.info('Running executable: %s', cmd_line) - return subprocess.run(cmd_line, check=True) - - -class BoostArchive: - def __init__(self, version, path): - self.version = version - self.path = path - - @property - def dir_name(self): - return self.version.dir_name - - def unpack(self, dest_dir): - path = os.path.join(dest_dir, self.dir_name) - if os.path.exists(path): - raise RuntimeError(f'Boost directory already exists: {path}') - logging.info('Unpacking Boost to: %s', path) - shutil.unpack_archive(self.path, dest_dir) - return BoostDir(path) - - -class ArchiveStorage(abc.ABC): - @contextmanager - def download(self, version): - path = self.get_archive(version) - if path is not None: - logging.info('Using existing Boost archive: %s', path) - yield BoostArchive(version, path) - return - - urls = version.get_download_urls() - - for url in urls: - logging.info('Trying URL: %s', url) - try: - with urllib.request.urlopen(url, timeout=20) as request: - with self.write_archive(version, request.read()) as path: - yield BoostArchive(version, path) - return - except urllib.request.URLError as e: - logging.error("Couldn't download from this mirror, an error occured:") - logging.exception(e) - - raise RuntimeError("Couldn't download Boost from any of the mirrors") - - @abc.abstractmethod - def get_archive(self, version): - pass - - @contextmanager - @abc.abstractmethod - def write_archive(self, version, contents): - pass - - -class CacheStorage(ArchiveStorage): - def __init__(self, cache_dir): - self._dir = cache_dir - - def _archive_path(self, version): - return os.path.join(self._dir, version.archive_name) - - def get_archive(self, version): - path = self._archive_path(version) - if os.path.exists(path): - return path - return None - - @contextmanager - def write_archive(self, version, contents): - path = self._archive_path(version) - logging.info('Writing Boost archive: %s', path) - if os.path.exists(path): - raise RuntimeError(f'cannot download Boost, file already exists: {path}') - with open(path, mode='w+b') as dest: - dest.write(contents) - yield path - - -class TempStorage(ArchiveStorage): - def __init__(self, temp_dir): - self._dir = temp_dir - - def get_archive(self, version): - return None - - @contextmanager - def write_archive(self, version, contents): - with tempfile.NamedTemporaryFile(prefix=f'boost_{version}_', suffix=version.archive_ext, dir=self._dir, delete=False) as dest: - path = dest.name - logging.info('Writing Boost archive: %s', path) - dest.write(contents) - try: - yield path - finally: - logging.info('Removing temporary Boost archive: %s', path) - os.remove(path) - - -class BoostDir: - def __init__(self, path): - if not os.path.isdir(path): - raise RuntimeError(f"Boost directory doesn't exist: {path}") - self.path = path - - def _go(self): - return _chdir(self.path) - - def build(self, params): - with self._go(): - self._bootstrap_if_required() - self._b2(params) - - def _bootstrap_if_required(self): - if os.path.isfile(self._b2_path()): - logging.info('Not going to bootstrap, b2 is already there') - return - self.bootstrap() - - def bootstrap(self): - with self._go(): - _run_executable(self._bootstrap_path()) - - def _b2(self, params): - for b2_params in params.enum_b2_args(): - _run_executable([self._b2_path()] + b2_params) - - @staticmethod - def _bootstrap_path(): - return os.path.join('.', BoostDir._bootstrap_name()) - - @staticmethod - def _bootstrap_name(): - ext = '.sh' - if _on_windows(): - ext = '.bat' - return f'bootstrap{ext}' - - @staticmethod - def _b2_path(): - return os.path.join('.', BoostDir._b2_name()) - - @staticmethod - def _b2_name(): - ext = '' - if _on_windows(): - ext = '.exe' - return f'b2{ext}' +import project.utils class BuildParameters: - def __init__(self, args): - self.platforms = args.platforms or Platform.all() - self.configurations = args.configurations or Configuration.all() - self.link = args.link or Linkage.all() - self.runtime_link = args.runtime_link - + def __init__(self, boost_dir, build_dir=None, platforms=None, configurations=None, link=None, + runtime_link=None, b2_args=None): + + boost_dir = project.utils.normalize_path(boost_dir) + if build_dir is not None: + build_dir = project.utils.normalize_path(build_dir) + platforms = platforms or Platform.all() + configurations = configurations or Configuration.all() + link = link or Linkage.all() + runtime_link = runtime_link or Linkage.STATIC + b2_args = b2_args or [] + + self.boost_dir = boost_dir self.stage_dir = 'stage' + self.build_dir = build_dir + self.platforms = platforms + self.configurations = configurations + self.link = link + self.runtime_link = runtime_link + self.b2_args = b2_args - self.build_dir = args.build_dir - self.boost_dir = args.boost_dir - - self.b2_args = args.b2_args + @staticmethod + def from_args(args): + return BuildParameters(**vars(args)) def enum_b2_args(self): with self._create_build_dir() as build_dir: @@ -250,7 +69,7 @@ class BuildParameters: if link is Linkage.SHARED: logging.warning("Cannot link the runtime statically to a dynamic library, going to link dynamically") runtime_link = Linkage.SHARED - elif _on_linux(): + elif project.utils.on_linux(): logging.warning("Cannot link to the GNU C Library (which is assumed) statically, going to link dynamically") runtime_link = Linkage.SHARED yield link, runtime_link @@ -316,8 +135,9 @@ class BuildParameters: return f'variant={configuration.to_boost_variant()}' -def _parse_dir(s): - return os.path.abspath(os.path.normpath(s)) +def build(params): + boost_dir = BoostDir(params.boost_dir) + boost_dir.build(params) def _parse_args(argv=None): @@ -329,93 +149,45 @@ def _parse_args(argv=None): description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - subparsers = parser.add_subparsers(dest='command') - - download = subparsers.add_parser('download', help='download & bootstrap Boost') - - download.add_argument('--cache', metavar='DIR', dest='cache_dir', - type=_parse_dir, - help='download directory (temporary file unless specified)') - download.add_argument('--unpack', metavar='DIR', dest='unpack_dir', - type=_parse_dir, default='.', - help='directory to unpack the archive to') - download.add_argument('boost_version', metavar='VERSION', - type=Version.from_string, - help='Boost version (in the MAJOR.MINOR.PATCH format)') - - build = subparsers.add_parser('build', help='build the libraries') - # These are used to put the built libraries into proper stage/ # subdirectories (to avoid name clashes). - build.add_argument('--platform', metavar='PLATFORM', - nargs='*', dest='platforms', default=[], - type=Platform.parse, - help=f'target platform ({"/".join(map(str, Platform))})') - build.add_argument('--configuration', metavar='CONFIGURATION', - nargs='*', dest='configurations', default=[], - type=Configuration.parse, - help=f'target configuration ({"/".join(map(str, Configuration))})') + parser.add_argument('--platform', metavar='PLATFORM', + nargs='*', dest='platforms', default=[], + type=Platform.parse, + help=f'target platform ({"/".join(map(str, Platform))})') + parser.add_argument('--configuration', metavar='CONFIGURATION', + nargs='*', dest='configurations', default=[], + type=Configuration.parse, + help=f'target configuration ({"/".join(map(str, Configuration))})') # This is needed because the default behaviour on Linux and Windows is # different: static & dynamic libs are built on Linux, but only static libs # are built on Windows by default. - build.add_argument('--link', metavar='LINKAGE', - nargs='*', default=[], - type=Linkage.parse, - help=f'how the libraries are linked ({"/".join(map(str, Linkage))})') + parser.add_argument('--link', metavar='LINKAGE', + nargs='*', default=[], + type=Linkage.parse, + help=f'how the libraries are linked ({"/".join(map(str, Linkage))})') # This is used to omit runtime-link=static I'd have to otherwise use a lot, # plus the script validates the link= and runtime-link= combinations. - build.add_argument('--runtime-link', metavar='LINKAGE', - type=Linkage.parse, default=Linkage.STATIC, - help=f'how the libraries link to the runtime ({"/".join(map(str, Linkage))})') - - build.add_argument('--build', metavar='DIR', dest='build_dir', - type=_parse_dir, - help='Boost build directory (temporary directory unless specified)') - build.add_argument('boost_dir', metavar='DIR', - type=_parse_dir, - help='root Boost directory') - - build.add_argument('b2_args', nargs='*', metavar='B2_ARG', default=[], - help='additional b2 arguments, to be passed verbatim') - - args = parser.parse_args(argv) - if args.command is None: - parser.error("please specify a command") - return args - - -def build(args): - build_params = BuildParameters(args) - boost_dir = BoostDir(args.boost_dir) - boost_dir.build(build_params) - + parser.add_argument('--runtime-link', metavar='LINKAGE', + type=Linkage.parse, default=Linkage.STATIC, + help=f'how the libraries link to the runtime ({"/".join(map(str, Linkage))})') -def download(args): - storage = TempStorage(args.unpack_dir) - if args.cache_dir is not None: - storage = CacheStorage(args.cache_dir) - with storage.download(args.boost_version) as archive: - boost_dir = archive.unpack(args.unpack_dir) - boost_dir.bootstrap() + parser.add_argument('--build', metavar='DIR', dest='build_dir', + type=project.utils.normalize_path, + help='Boost build directory (temporary directory unless specified)') + parser.add_argument('boost_dir', metavar='DIR', + type=project.utils.normalize_path, + help='root Boost directory') + parser.add_argument('b2_args', nargs='*', metavar='B2_ARG', default=[], + help='additional b2 arguments, to be passed verbatim') -def main(argv=None): - args = _parse_args(argv) - if args.command == 'download': - download(args) - elif args.command == 'build': - build(args) - else: - raise NotImplementedError(f'unsupported command: {args.command}') + return parser.parse_args(argv) def _main(argv=None): - _setup_logging() - try: - main(argv) - except Exception as e: - logging.exception(e) - raise + with project.utils.setup_logging(): + build(BuildParameters.from_args(_parse_args(argv))) if __name__ == '__main__': diff --git a/project/boost/directory.py b/project/boost/directory.py new file mode 100644 index 0000000..e50041f --- /dev/null +++ b/project/boost/directory.py @@ -0,0 +1,60 @@ +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +import logging +import os.path + +from project.utils import cd, run, on_windows + + +class BoostDir: + def __init__(self, path): + if not os.path.isdir(path): + raise RuntimeError(f"Boost directory doesn't exist: {path}") + self.path = path + + def _go(self): + return cd(self.path) + + def build(self, params): + with self._go(): + self._bootstrap_if_required() + self._b2(params) + + def _bootstrap_if_required(self): + if os.path.isfile(self._b2_path()): + logging.info('Not going to bootstrap, b2 is already there') + return + self.bootstrap() + + def bootstrap(self): + with self._go(): + run(self._bootstrap_path()) + + def _b2(self, params): + for b2_params in params.enum_b2_args(): + run([self._b2_path()] + b2_params) + + @staticmethod + def _bootstrap_path(): + return os.path.join('.', BoostDir._bootstrap_name()) + + @staticmethod + def _bootstrap_name(): + ext = '.sh' + if on_windows(): + ext = '.bat' + return f'bootstrap{ext}' + + @staticmethod + def _b2_path(): + return os.path.join('.', BoostDir._b2_name()) + + @staticmethod + def _b2_name(): + ext = '' + if on_windows(): + ext = '.exe' + return f'b2{ext}' diff --git a/project/boost/download.py b/project/boost/download.py new file mode 100644 index 0000000..954ec1f --- /dev/null +++ b/project/boost/download.py @@ -0,0 +1,112 @@ +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +R'''Download & bootstrap Boost. + +This script downloads and bootstraps a Boost distribution. It's main utility +is that it's supposed to be cross-platform. + +Usage examples: + + $ %(prog)s 1.71.0 + ... + + $ %(prog)s --unpack ~/workspace/third-party/ 1.65.0 + ... +''' + +import argparse +from contextlib import contextmanager +import logging +import sys +import urllib.request + +from project.boost.archive import Archive, PermanentStorage, TemporaryStorage +from project.boost.version import Version +import project.utils + + +def _download_try_url(url): + logging.info('Trying URL: %s', url) + try: + with urllib.request.urlopen(url, timeout=20) as request: + return request.read() + except urllib.request.URLError as e: + logging.error("Couldn't download from this mirror, an error occured:") + logging.exception(e) + + +@contextmanager +def _download_try_all_urls(version, storage): + urls = version.get_download_urls() + for url in urls: + reply = _download_try_url(url) + if reply is None: + continue + with storage.write_archive(version, reply) as path: + yield path + return + raise RuntimeError("Couldn't download Boost from any of the mirrors") + + +@contextmanager +def _download_if_necessary(version, storage): + path = storage.get_archive(version) + if path is not None: + logging.info('Using existing Boost archive: %s', path) + yield path + return + with _download_try_all_urls(version, storage) as path: + yield path + + +class DownloadParameters: + def __init__(self, version, unpack_dir='.', cache_dir=None): + self.version = version + self.unpack_dir = project.utils.normalize_path(unpack_dir) + self.storage = TemporaryStorage(unpack_dir) + if cache_dir is not None: + cache_dir = project.utils.normalize_path(cache_dir) + self.storage = PermanentStorage(cache_dir) + + @staticmethod + def from_args(args): + return DownloadParameters(**vars(args)) + + +def download(params): + with _download_if_necessary(params.version, params.storage) as path: + archive = Archive(params.version, path) + boost_dir = archive.unpack(params.unpack_dir) + boost_dir.bootstrap() + + +def _parse_args(argv=None): + if argv is None: + argv = sys.argv[1:] + logging.info('Command line arguments: %s', argv) + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument('--unpack', metavar='DIR', dest='unpack_dir', + type=project.utils.normalize_path, default='.', + help='directory to unpack the archive to') + parser.add_argument('--cache', metavar='DIR', dest='cache_dir', + type=project.utils.normalize_path, + help='download directory (temporary file unless specified)') + parser.add_argument('version', metavar='VERSION', type=Version.from_string, + help='Boost version (in the MAJOR.MINOR.PATCH format)') + return parser.parse_args(argv) + + +def _main(argv=None): + with project.utils.setup_logging(): + download(DownloadParameters.from_args(_parse_args(argv))) + + +if __name__ == '__main__': + _main() -- cgit v1.2.3