summaryrefslogtreecommitdiff
path: root/hadrian/bootstrap/bootstrap.py
diff options
context:
space:
mode:
Diffstat (limited to 'hadrian/bootstrap/bootstrap.py')
-rwxr-xr-xhadrian/bootstrap/bootstrap.py234
1 files changed, 170 insertions, 64 deletions
diff --git a/hadrian/bootstrap/bootstrap.py b/hadrian/bootstrap/bootstrap.py
index 8ed4d5588c..d5580ab484 100755
--- a/hadrian/bootstrap/bootstrap.py
+++ b/hadrian/bootstrap/bootstrap.py
@@ -21,6 +21,8 @@ from pathlib import Path
import platform
import shutil
import subprocess
+import tempfile
+import sys
from textwrap import dedent
from typing import Set, Optional, Dict, List, Tuple, \
NewType, BinaryIO, NamedTuple, TypeVar
@@ -31,7 +33,7 @@ BUILDDIR = Path('_build')
BINDIR = BUILDDIR / 'bin' # binaries go there (--bindir)
DISTDIR = BUILDDIR / 'dists' # --builddir
-UNPACKED = BUILDDIR / 'unpacked' # where we unpack tarballs
+UNPACKED = BUILDDIR / 'unpacked' # where we unpack final package tarballs
TARBALLS = BUILDDIR / 'tarballs' # where we download tarballks
PSEUDOSTORE = BUILDDIR / 'pseudostore' # where we install packages
ARTIFACTS = BUILDDIR / 'artifacts' # Where we put the archive
@@ -46,6 +48,8 @@ class PackageSource(Enum):
HACKAGE = 'hackage'
LOCAL = 'local'
+url = str
+
BuiltinDep = NamedTuple('BuiltinDep', [
('package', PackageName),
('version', Version),
@@ -68,10 +72,18 @@ BootstrapInfo = NamedTuple('BootstrapInfo', [
('dependencies', List[BootstrapDep]),
])
+FetchInfo = NamedTuple('FetchInfo', [
+ ('url', url),
+ ('sha256', SHA256Hash)
+])
+
+FetchPlan = Dict[Path, FetchInfo]
+
class Compiler:
def __init__(self, ghc_path: Path):
if not ghc_path.is_file():
- raise TypeError(f'GHC {ghc_path} is not a file')
+ print(f'GHC {ghc_path} is not a file')
+ sys.exit(1)
self.ghc_path = ghc_path.resolve()
@@ -111,40 +123,11 @@ def package_cabal_url(package: PackageName, version: Version, revision: int) ->
return f'http://hackage.haskell.org/package/{package}-{version}/revision/{revision}.cabal'
def verify_sha256(expected_hash: SHA256Hash, f: Path):
+ print(f"Verifying {f}...")
h = hash_file(hashlib.sha256(), f.open('rb'))
if h != expected_hash:
raise BadTarball(f, expected_hash, h)
-def fetch_package(package: PackageName,
- version: Version,
- src_sha256: SHA256Hash,
- revision: Optional[int],
- cabal_sha256: Optional[SHA256Hash],
- ) -> (Path, Path):
- import urllib.request
-
- # Download source distribution
- tarball = TARBALLS / f'{package}-{version}.tar.gz'
- if not tarball.exists():
- print(f'Fetching {package}-{version}...')
- tarball.parent.mkdir(parents=True, exist_ok=True)
- url = package_url(package, version)
- with urllib.request.urlopen(url) as resp:
- shutil.copyfileobj(resp, tarball.open('wb'))
-
- verify_sha256(src_sha256, tarball)
-
- # Download revised cabal file
- cabal_file = TARBALLS / f'{package}.cabal'
- if revision is not None and not cabal_file.exists():
- assert cabal_sha256 is not None
- url = package_cabal_url(package, version, revision)
- with urllib.request.urlopen(url) as resp:
- shutil.copyfileobj(resp, cabal_file.open('wb'))
- verify_sha256(cabal_sha256, cabal_file)
-
- return (tarball, cabal_file)
-
def read_bootstrap_info(path: Path) -> BootstrapInfo:
obj = json.load(path.open())
@@ -166,15 +149,18 @@ def check_builtin(dep: BuiltinDep, ghc: Compiler) -> None:
print(f'Using {dep.package}-{dep.version} from GHC...')
return
-def install_dep(dep: BootstrapDep, ghc: Compiler) -> None:
- dist_dir = (DISTDIR / f'{dep.package}-{dep.version}').resolve()
-
+def resolve_dep(dep : BootstrapDep) -> Path:
if dep.source == PackageSource.HACKAGE:
- assert dep.src_sha256 is not None
- (tarball, cabal_file) = fetch_package(dep.package, dep.version, dep.src_sha256,
- dep.revision, dep.cabal_sha256)
+
+ tarball = TARBALLS / f'{dep.package}-{dep.version}.tar.gz'
+ verify_sha256(dep.src_sha256, tarball)
+
+ cabal_file = TARBALLS / f'{dep.package}.cabal'
+ verify_sha256(dep.cabal_sha256, cabal_file)
+
UNPACKED.mkdir(parents=True, exist_ok=True)
shutil.unpack_archive(tarball.resolve(), UNPACKED, 'gztar')
+
sdist_dir = UNPACKED / f'{dep.package}-{dep.version}'
# Update cabal file with revision
@@ -183,9 +169,16 @@ def install_dep(dep: BootstrapDep, ghc: Compiler) -> None:
elif dep.source == PackageSource.LOCAL:
if dep.package == 'hadrian':
- sdist_dir = Path('../').resolve()
+ sdist_dir = Path(sys.path[0]).parent.resolve()
else:
raise ValueError(f'Unknown local package {dep.package}')
+ return sdist_dir
+
+
+def install_dep(dep: BootstrapDep, ghc: Compiler) -> None:
+ dist_dir = (DISTDIR / f'{dep.package}-{dep.version}').resolve()
+
+ sdist_dir = resolve_dep(dep)
install_sdist(dist_dir, sdist_dir, ghc, dep.flags)
@@ -298,7 +291,6 @@ def archive_name(version):
return f'hadrian-{version}-{machine}-{version}'
def make_archive(hadrian_path):
- import tempfile
print(f'Creating distribution tarball')
@@ -324,28 +316,88 @@ def make_archive(hadrian_path):
return archivename
+def fetch_from_plan(plan : FetchPlan, output_dir : Path):
+ import urllib.request
+
+ output_dir.resolve()
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ for path in plan:
+ output_path = output_dir / path
+ url = plan[path].url
+ sha = plan[path].sha256
+ if not output_path.exists():
+ print(f'Fetching {url}...')
+ with urllib.request.urlopen(url) as resp:
+ shutil.copyfileobj(resp, output_path.open('wb'))
+ verify_sha256(sha, output_path)
+
+def gen_fetch_plan(info : BootstrapInfo) -> FetchPlan :
+ sources_dict = {}
+ for dep in info.dependencies:
+ if dep.package != 'hadrian':
+ sources_dict[f"{dep.package}-{dep.version}.tar.gz"] = FetchInfo(package_url(dep.package, dep.version), dep.src_sha256)
+ if dep.revision is not None:
+ sources_dict[f"{dep.package}.cabal"] = FetchInfo(package_cabal_url(dep.package, dep.version, dep.revision), dep.cabal_sha256)
+ return sources_dict
+
+def find_ghc(compiler) -> Compiler:
+ # Find compiler
+ if compiler is None:
+ path = shutil.which('ghc')
+ if path is None:
+ raise ValueError("Couldn't find ghc in PATH")
+ ghc = Compiler(Path(path))
+ else:
+ ghc = Compiler(compiler)
+ return ghc
+
+
def main() -> None:
import argparse
parser = argparse.ArgumentParser(
description="bootstrapping utility for hadrian.",
epilog = USAGE,
formatter_class = argparse.RawDescriptionHelpFormatter)
- parser.add_argument('-d', '--deps', type=Path, default='bootstrap-deps.json',
- help='bootstrap dependency file')
+ parser.add_argument('-d', '--deps', type=Path, help='bootstrap dependency file (plan-bootstrap.json)')
parser.add_argument('-w', '--with-compiler', type=Path,
help='path to GHC')
- args = parser.parse_args()
+ parser.add_argument('-s', '--bootstrap-sources', type=Path,
+ help='Path to prefetched bootstrap sources tarball')
- # Find compiler
- if args.with_compiler is None:
- path = shutil.which('ghc')
- if path is None:
- raise ValueError("Couldn't find ghc in PATH")
- ghc = Compiler(Path(path))
- else:
- ghc = Compiler(args.with_compiler)
+ subparsers = parser.add_subparsers(dest="command")
+
+ parser_list = subparsers.add_parser('list-sources', help='list all sources required to download')
+ parser_list.add_argument('-o','--output', type=Path, default='fetch_plan.json')
+
+ parser_fetch = subparsers.add_parser('fetch', help='fetch all required sources from hackage (for offline builds)')
+ parser_fetch.add_argument('-o','--output', type=Path, default='bootstrap-sources')
+ parser_fetch.add_argument('-p','--fetch-plan', type=Path, default=None, help="A json document that lists the urls required for download (optional)")
+
+ args = parser.parse_args()
- print(f'Bootstrapping hadrian with GHC {ghc.version} at {ghc.ghc_path}...')
+ ghc = None
+
+ if args.deps is None:
+ if args.bootstrap_sources is None:
+ # find appropriate plan in the same directory as the script
+ ghc = find_ghc(args.with_compiler)
+ args.deps = Path(sys.path[0]) / f"plan-bootstrap-{ghc.version.replace('.','_')}.json"
+ print(f"defaulting bootstrap plan to {args.deps}")
+ # We have a tarball with all the required information, unpack it and use for further
+ elif args.bootstrap_sources is not None and args.command != 'list-sources':
+ print(f'Unpacking {args.bootstrap_sources} to {TARBALLS}')
+ shutil.unpack_archive(args.bootstrap_sources.resolve(), TARBALLS, 'gztar')
+ args.deps = TARBALLS / 'plan-bootstrap.json'
+ print(f"using plan-bootstrap.json ({args.deps}) from {args.bootstrap_sources}")
+ else:
+ print("We need a bootstrap plan (plan-bootstrap.json) or a tarball containing bootstrap information")
+ print("Perhaps pick an appropriate one from: ")
+ for child in Path(sys.path[0]).iterdir:
+ if child.match('plan-bootstrap-*.json'):
+ print(child)
+ sys.exit(1)
+ info = read_bootstrap_info(args.deps)
print(dedent("""
DO NOT use this script if you have another recent cabal-install available.
@@ -353,25 +405,79 @@ def main() -> None:
architectures.
"""))
- info = read_bootstrap_info(args.deps)
- bootstrap(info, ghc)
- hadrian_path = (BINDIR / 'hadrian').resolve()
- archive = make_archive(hadrian_path)
+ if(args.command == 'fetch'):
+ if args.fetch_plan is not None:
+ plan = { path : FetchInfo(p["url"],p["sha256"]) for path, p in json.load(args.fetch_plan.open()).items() }
+ else:
+ plan = gen_fetch_plan(info)
+
+ # In temporary directory, create a directory which we will archive
+ tmpdir = TMPDIR.resolve()
+ tmpdir.mkdir(parents=True, exist_ok=True)
+
+ rootdir = Path(tempfile.mkdtemp(dir=tmpdir))
+
+ fetch_from_plan(plan, rootdir)
+
+ shutil.copyfile(args.deps, rootdir / 'plan-bootstrap.json')
- print(dedent(f'''
- Bootstrapping finished!
+ fmt = 'gztar'
+ if platform.system() == 'Windows': fmt = 'zip'
+
+ archivename = shutil.make_archive(args.output, fmt, root_dir=rootdir)
- The resulting hadrian executable can be found at
+ print(f'Bootstrap sources saved to {archivename}')
+ print(f'Use `bootstrap.py -d {args.deps} -s {archivename}` to continue')
- {hadrian_path}
+ elif(args.command == 'list-sources'):
+ plan = gen_fetch_plan(info)
+ with open(args.output, 'w') as out:
+ json.dump({path : val._asdict() for path,val in plan.items()}, out)
+ print(f"Required hackage sources saved to {args.output}")
+ tarfmt= "\n./"
+ print(f"""
+Download the files listed in {args.output} and save them to a tarball ($TARBALL), along with {args.deps}
+The contents of $TARBALL should look like:
- It have been archived for distribution in
+./
+./plan-bootstrap.json
+./{tarfmt.join(path for path in plan)}
- {archive}
+Then use `bootstrap.py -s $TARBALL` to continue
+Alternatively, you could use `bootstrap.py -d {args.deps} fetch -o $TARBALL` to download and generate the tarball, skipping this step
+""")
- You can use this executable to build GHC.
- '''))
+ elif(args.command == None):
+ if ghc is None:
+ ghc = find_ghc(args.with_compiler)
+
+ print(f'Bootstrapping hadrian with GHC {ghc.version} at {ghc.ghc_path}...')
+
+ if args.bootstrap_sources is None:
+ plan = gen_fetch_plan(info)
+ fetch_from_plan(plan, TARBALLS)
+
+ bootstrap(info, ghc)
+ hadrian_path = (BINDIR / 'hadrian').resolve()
+
+ archive = make_archive(hadrian_path)
+
+ print(dedent(f'''
+ Bootstrapping finished!
+
+ The resulting hadrian executable can be found at
+
+ {hadrian_path}
+
+ It have been archived for distribution in
+
+ {archive}
+
+ You can use this executable to build GHC.
+ '''))
+ else:
+ print(f"No such command: {args.command}")
def subprocess_run(args, **kwargs):
"Like subprocess.run, but also print what we run"