Skip to content
Snippets Groups Projects
Unverified Commit 26c4285e authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub
Browse files

Merge pull request #8368 from sheaf/offline-bootstrap

Allow offline bootstrapping of cabal-install
parents 55b4ed9f 3f929965
No related branches found
No related tags found
No related merge requests found
...@@ -32,8 +32,12 @@ jobs: ...@@ -32,8 +32,12 @@ jobs:
GHC_VERSION=${{ matrix.ghc }} GHC_VERSION=${{ matrix.ghc }}
ghcup config set cache true ghcup config set cache true
ghcup install ghc $GHC_VERSION ghcup install ghc $GHC_VERSION
# We use linux dependencies also on macos
python3 bootstrap/bootstrap.py -w $(ghcup whereis ghc $GHC_VERSION) -d bootstrap/linux-$GHC_VERSION.json # Fetch the bootstrap sources (we use linux dependencies also on macos)
python3 bootstrap/bootstrap.py -w $(ghcup whereis ghc $GHC_VERSION) -d bootstrap/linux-$GHC_VERSION.json fetch
# Bootstrap using the bootstrap sources
python3 bootstrap/bootstrap.py -w $(ghcup whereis ghc $GHC_VERSION) --bootstrap-sources bootstrap-sources.tar.gz
- name: Smoke test - name: Smoke test
run: | run: |
......
...@@ -5,15 +5,24 @@ on a new platform. If you already have a functional (if dated) cabal-install ...@@ -5,15 +5,24 @@ on a new platform. If you already have a functional (if dated) cabal-install
please rather run `cabal v2-install`. please rather run `cabal v2-install`.
The typical usage is porting to a new linux architecture, The typical usage is porting to a new linux architecture,
then the `linux-$GHCVER.json` file is available in `bootstrap/` folder: then the `linux-{ghc-ver}.json` file is available in the `bootstrap/` folder:
On a (linux) system you are bootstrapping, run On a (linux) system you are bootstrapping, run
./bootstrap/bootstrap.py -d ./bootstrap/linux-$GHCVER.json -w /path/to-ghc ./bootstrap/bootstrap.py -d ./bootstrap/linux-ghcver.json -w /path/to-ghc
from the top directory of the source checkout. from the top directory of the source checkout.
To generate the `$PLATFORM-$GHCVER` files for other platforms, do: For offline builds, you can first run
./bootstrap/bootstrap.py -d ./bootstrap/linux-ghcver.json -w /path/to-ghc fetch
to fetch tarballs for all the dependencies. These can then be used by a further
bootstrap command by way of the `--bootstrap-sources` argument:
./bootstrap/bootstrap.py -w /path/to-ghc --bootstrap-sources bootstrap-sources.tar.gz
To generate the `platform-{ghc-ver}` files for other platforms, do:
1. On a system with functional cabal-install, install the same GHC version 1. On a system with functional cabal-install, install the same GHC version
as you will use to bootstrap on the host system. as you will use to bootstrap on the host system.
......
...@@ -13,17 +13,20 @@ on a new platform. If you already have a functional (if dated) cabal-install ...@@ -13,17 +13,20 @@ on a new platform. If you already have a functional (if dated) cabal-install
please rather run `cabal v2-install .`. please rather run `cabal v2-install .`.
""" """
import argparse
from enum import Enum from enum import Enum
import hashlib import hashlib
import logging
import json import json
from pathlib import Path from pathlib import Path
import platform import platform
import shutil import shutil
import subprocess import subprocess
import sys
import tempfile
import urllib.request
from textwrap import dedent from textwrap import dedent
from typing import Set, Optional, Dict, List, Tuple, \ from typing import Optional, Dict, List, Tuple, \
NewType, BinaryIO, NamedTuple, TypeVar NewType, BinaryIO, NamedTuple
#logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.INFO)
...@@ -68,6 +71,15 @@ BootstrapInfo = NamedTuple('BootstrapInfo', [ ...@@ -68,6 +71,15 @@ BootstrapInfo = NamedTuple('BootstrapInfo', [
('dependencies', List[BootstrapDep]), ('dependencies', List[BootstrapDep]),
]) ])
FetchInfo = NamedTuple('FetchInfo', [
('url', str),
('sha256', SHA256Hash)
])
FetchPlan = Dict[Path, FetchInfo]
local_packages: List[PackageName] = ["Cabal-syntax", "Cabal", "cabal-install-solver", "cabal-install"]
class Compiler: class Compiler:
def __init__(self, ghc_path: Path): def __init__(self, ghc_path: Path):
if not ghc_path.is_file(): if not ghc_path.is_file():
...@@ -75,14 +87,17 @@ class Compiler: ...@@ -75,14 +87,17 @@ class Compiler:
self.ghc_path = ghc_path.resolve() self.ghc_path = ghc_path.resolve()
exe = ''
if platform.system() == 'Windows': exe = '.exe'
info = self._get_ghc_info() info = self._get_ghc_info()
self.version = info['Project version'] self.version = info['Project version']
#self.lib_dir = Path(info['LibDir']) #self.lib_dir = Path(info['LibDir'])
#self.ghc_pkg_path = (self.lib_dir / 'bin' / 'ghc-pkg').resolve() #self.ghc_pkg_path = (self.lib_dir / 'bin' / 'ghc-pkg').resolve()
self.ghc_pkg_path = (self.ghc_path.parent / 'ghc-pkg').resolve() self.ghc_pkg_path = (self.ghc_path.parent / ('ghc-pkg' + exe)).resolve()
if not self.ghc_pkg_path.is_file(): if not self.ghc_pkg_path.is_file():
raise TypeError(f'ghc-pkg {self.ghc_pkg_path} is not a file') raise TypeError(f'ghc-pkg {self.ghc_pkg_path} is not a file')
self.hsc2hs_path = (self.ghc_path.parent / 'hsc2hs').resolve() self.hsc2hs_path = (self.ghc_path.parent / ('hsc2hs' + exe)).resolve()
if not self.hsc2hs_path.is_file(): if not self.hsc2hs_path.is_file():
raise TypeError(f'hsc2hs {self.hsc2hs_path} is not a file') raise TypeError(f'hsc2hs {self.hsc2hs_path} is not a file')
...@@ -118,36 +133,6 @@ def verify_sha256(expected_hash: SHA256Hash, f: Path): ...@@ -118,36 +133,6 @@ def verify_sha256(expected_hash: SHA256Hash, f: Path):
if h != expected_hash: if h != expected_hash:
raise BadTarball(f, expected_hash, h) 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: def read_bootstrap_info(path: Path) -> BootstrapInfo:
obj = json.load(path.open()) obj = json.load(path.open())
...@@ -169,13 +154,15 @@ def check_builtin(dep: BuiltinDep, ghc: Compiler) -> None: ...@@ -169,13 +154,15 @@ def check_builtin(dep: BuiltinDep, ghc: Compiler) -> None:
print(f'Using {dep.package}-{dep.version} from GHC...') print(f'Using {dep.package}-{dep.version} from GHC...')
return return
def install_dep(dep: BootstrapDep, ghc: Compiler) -> None: def resolve_dep(dep : BootstrapDep) -> Path:
dist_dir = (DISTDIR / f'{dep.package}-{dep.version}').resolve()
if dep.source == PackageSource.HACKAGE: if dep.source == PackageSource.HACKAGE:
assert dep.src_sha256 is not None
(tarball, cabal_file) = fetch_package(dep.package, dep.version, dep.src_sha256, tarball = TARBALLS / f'{dep.package}-{dep.version}.tar.gz'
dep.revision, dep.cabal_sha256) 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) UNPACKED.mkdir(parents=True, exist_ok=True)
shutil.unpack_archive(tarball.resolve(), UNPACKED, 'gztar') shutil.unpack_archive(tarball.resolve(), UNPACKED, 'gztar')
sdist_dir = UNPACKED / f'{dep.package}-{dep.version}' sdist_dir = UNPACKED / f'{dep.package}-{dep.version}'
...@@ -191,16 +178,16 @@ def install_dep(dep: BootstrapDep, ghc: Compiler) -> None: ...@@ -191,16 +178,16 @@ def install_dep(dep: BootstrapDep, ghc: Compiler) -> None:
f.write('main = defaultMain\n') f.write('main = defaultMain\n')
elif dep.source == PackageSource.LOCAL: elif dep.source == PackageSource.LOCAL:
if dep.package == 'Cabal': if dep.package in local_packages:
sdist_dir = Path('Cabal').resolve() sdist_dir = Path(dep.package).resolve()
elif dep.package == 'Cabal-syntax':
sdist_dir = Path('Cabal-syntax').resolve()
elif dep.package == 'cabal-install-solver':
sdist_dir = Path('cabal-install-solver').resolve()
elif dep.package == 'cabal-install':
sdist_dir = Path('cabal-install').resolve()
else: else:
raise ValueError(f'Unknown local package {dep.package}') 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) install_sdist(dist_dir, sdist_dir, ghc, dep.flags)
...@@ -307,7 +294,7 @@ def archive_name(cabalversion): ...@@ -307,7 +294,7 @@ def archive_name(cabalversion):
return f'cabal-install-{cabalversion}-{machine}-{version}' return f'cabal-install-{cabalversion}-{machine}-{version}'
def make_archive(cabal_path): def make_distribution_archive(cabal_path):
import tempfile import tempfile
print(f'Creating distribution tarball') print(f'Creating distribution tarball')
...@@ -334,28 +321,62 @@ def make_archive(cabal_path): ...@@ -334,28 +321,62 @@ def make_archive(cabal_path):
return archivename return archivename
def fetch_from_plan(plan : FetchPlan, output_dir : Path):
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 not(dep.package in local_packages):
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:
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: def main() -> None:
import argparse
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="bootstrapping utility for cabal-install.", description="bootstrapping utility for cabal-install.",
epilog = USAGE, epilog = USAGE,
formatter_class = argparse.RawDescriptionHelpFormatter) formatter_class = argparse.RawDescriptionHelpFormatter)
parser.add_argument('-d', '--deps', type=Path, default='bootstrap-deps.json', parser.add_argument('-d', '--deps', type=Path,
help='bootstrap dependency file') help='bootstrap dependency file')
parser.add_argument('-w', '--with-compiler', type=Path, parser.add_argument('-w', '--with-compiler', type=Path,
help='path to GHC') help='path to GHC')
args = parser.parse_args() parser.add_argument('-s', '--bootstrap-sources', type=Path,
help='path to prefetched bootstrap sources archive')
parser.add_argument('--archive', dest='want_archive', action='store_true')
parser.add_argument('--no-archive', dest='want_archive', action='store_false')
parser.set_defaults(want_archive=True)
# Find compiler subparsers = parser.add_subparsers(dest="command")
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)
print(f'Bootstrapping cabal-install with GHC {ghc.version} at {ghc.ghc_path}...') parser_fetch = subparsers.add_parser('build', help='build cabal-install (default)')
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')
args = parser.parse_args()
print(dedent(""" print(dedent("""
DO NOT use this script if you have another recent cabal-install available. DO NOT use this script if you have another recent cabal-install available.
...@@ -363,26 +384,82 @@ def main() -> None: ...@@ -363,26 +384,82 @@ def main() -> None:
architectures. architectures.
""")) """))
ghc = find_ghc(args.with_compiler)
sources_fmt = 'gztar'
if platform.system() == 'Windows': sources_fmt = 'zip'
if args.deps is None:
# We have a tarball with all the required information, unpack it
if args.bootstrap_sources is not None:
print(f'Unpacking {args.bootstrap_sources} to {TARBALLS}')
shutil.unpack_archive(args.bootstrap_sources.resolve(), TARBALLS, sources_fmt)
args.deps = TARBALLS / 'plan-bootstrap.json'
print(f"using plan-bootstrap.json ({args.deps}) from {args.bootstrap_sources}")
else:
print("The bootstrap script requires a bootstrap plan JSON file.")
print("See bootstrap/README.md for more information.")
sys.exit(1)
info = read_bootstrap_info(args.deps) info = read_bootstrap_info(args.deps)
bootstrap(info, ghc)
cabal_path = (BINDIR / 'cabal').resolve()
archive = make_archive(cabal_path) if args.command == 'fetch':
plan = gen_fetch_plan(info)
print(f'Fetching sources to bootstrap cabal-install with GHC {ghc.version} at {ghc.ghc_path}...')
# 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')
archivename = shutil.make_archive(args.output, sources_fmt, root_dir=rootdir)
print(dedent(f"""
Bootstrap sources saved to {archivename}
Use these with the command:
bootstrap.py -w {ghc.ghc_path} -s {archivename}
"""))
else: # 'build' command (default behaviour)
print(f'Bootstrapping cabal-install 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)
cabal_path = (BINDIR / 'cabal').resolve()
print(dedent(f'''
Bootstrapping finished!
print(dedent(f''' The resulting cabal-install executable can be found at
Bootstrapping finished!
The resulting cabal-install executable can be found at {cabal_path}
'''))
{cabal_path} if args.want_archive:
dist_archive = make_distribution_archive(cabal_path)
It have been archived for distribution in print(dedent(f'''
The cabal-install executable has been archived for distribution in
{archive} {dist_archive}
'''))
You now should use this to build a full cabal-install distribution print(dedent(f'''
using v2-build. You now should use this to build a full cabal-install distribution
''')) using v2-build.
'''))
def subprocess_run(args, **kwargs): def subprocess_run(args, **kwargs):
"Like subprocess.run, but also print what we run" "Like subprocess.run, but also print what we run"
......
synopsis: Allow offline bootstrapping of cabal-install
prs: #8368
packages: cabal-install
description: {
- The bootstrap script for cabal-install now supports fetching the sources of the dependencies in a separate step.
One can then copy over the resulting archive and perform offline bootstrapping of cabal-install.
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment