diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 67e7f7ce4ddc4ef2910f05344e2b6125cfb742b7..255223629fe27cfe858b05512dfc3eb3ae64ca1b 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -23,26 +23,20 @@ jobs: - name: Set PATH run: | echo "::add-path::$HOME/.cabal/bin" + - name: Install newer Python3 + run: | + add-apt-repository ppa:deadsnakes/ppa + apt-get update + apt-get install -y python3.8 - name: Update Hackage index run: cabal v2-update - uses: actions/checkout@v2 - - name: Release project - run: | - cp cabal.project.release cabal.project - rm -rf cabal.project.local cabal.project.freeze - - name: Build - run: | - cabal v2-build cabal-install:exe:cabal - cp $(find dist-newstyle -type f -executable -name cabal) cabal.exe - - name: Smoke test - run: | - ./cabal.exe --version - - name: Prepare for upload - run: xz -c < cabal.exe > cabal-artifact.xz - - uses: actions/upload-artifact@v1 + - name: Package project + run: python3.8 release.py + - uses: actions/upload-artifact@v2 with: - name: cabal-linux-x86_64.xz - path: cabal-artifact.xz + name: cabal-linux + path: _build/artifacts/* artifact-macos: name: Artifact on macOS @@ -72,24 +66,12 @@ jobs: - name: Update Hackage index run: cabal v2-update - uses: actions/checkout@v2 - - name: Release project - run: | - cp cabal.project.release cabal.project - rm -rf cabal.project.local cabal.project.freeze - - name: Build - run: | - cabal v2-build cabal-install:exe:cabal - # macOS find doesn't know -executable - cp $(find dist-newstyle -type f -name cabal) cabal.exe - - name: Smoke test - run: | - ./cabal.exe --version - - name: Prepare for upload - run: xz -c < cabal.exe > cabal-artifact.xz - - uses: actions/upload-artifact@v1 + - name: Package project + run: python3 release.py + - uses: actions/upload-artifact@v2 with: - name: cabal-macos-x86_64.xz - path: cabal-artifact.xz + name: cabal-macos + path: _build/artifacts/* artifact-windows: name: Artifact on Windows @@ -114,24 +96,9 @@ jobs: - name: Update Hackage index run: cabal v2-update - uses: actions/checkout@v2 - - name: Release project - shell: bash - run: | - cp cabal.project.release cabal.project - rm -rf cabal.project.local cabal.project.freeze - - name: Build - shell: bash - run: | - cabal v2-build cabal-install:exe:cabal - cp dist-newstyle/build/x86_64-windows/ghc-8.6.5/cabal-install-3.4.0.0/x/cabal/build/cabal/cabal.exe cabal.exe - - name: Smoke test - shell: bash - run: | - ./cabal.exe --version - - name: Prepare for upload - shell: bash - run: xz -c < cabal.exe > cabal-artifact.xz - - uses: actions/upload-artifact@v1 + - name: Package project + run: python release.py + - uses: actions/upload-artifact@v2 with: - name: cabal-windows-x86_64.xz - path: cabal-artifact.xz + name: cabal-macos + path: _build/artifacts/* diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index 93e0c6b2087ff1dcf27889a93ecca3eee5da0f26..257177d9ad3d0286bfaa2af6ce7e002abb07110d 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -27,9 +27,15 @@ jobs: - name: bootstrap.py run: | python3 bootstrap/bootstrap.py -w /opt/ghc/8.6.5/bin/ghc -d bootstrap/linux-8.6.5.json + - name: Smoke test run: | - packages/tmp/bin/cabal --version + _build/bin/cabal --version + + - uses: actions/upload-artifact@v2 + with: + name: cabal-linux-bootstrapped + path: _build/artifacts/* boostrap-macos: name: Bootstrap on macOS @@ -52,4 +58,9 @@ jobs: - name: Smoke test run: | - packages/tmp/bin/cabal --version + _build/bin/cabal --version + + - uses: actions/upload-artifact@v2 + with: + name: cabal-macos-bootstrapped + path: _build/artifacts/* diff --git a/.gitignore b/.gitignore index 0374201a3b75d503a19ea078170cb14f0653b6b9..d88300cc95b56e97f7ca7334ab238a62e10af782 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ cabal-tests.log /cabal-install/Setup /cabal-install/source-file-list +# Output of release and bootstrap +_build # editor temp files diff --git a/boot/ci-artifacts.template.yml b/boot/ci-artifacts.template.yml index 67e7f7ce4ddc4ef2910f05344e2b6125cfb742b7..255223629fe27cfe858b05512dfc3eb3ae64ca1b 100644 --- a/boot/ci-artifacts.template.yml +++ b/boot/ci-artifacts.template.yml @@ -23,26 +23,20 @@ jobs: - name: Set PATH run: | echo "::add-path::$HOME/.cabal/bin" + - name: Install newer Python3 + run: | + add-apt-repository ppa:deadsnakes/ppa + apt-get update + apt-get install -y python3.8 - name: Update Hackage index run: cabal v2-update - uses: actions/checkout@v2 - - name: Release project - run: | - cp cabal.project.release cabal.project - rm -rf cabal.project.local cabal.project.freeze - - name: Build - run: | - cabal v2-build cabal-install:exe:cabal - cp $(find dist-newstyle -type f -executable -name cabal) cabal.exe - - name: Smoke test - run: | - ./cabal.exe --version - - name: Prepare for upload - run: xz -c < cabal.exe > cabal-artifact.xz - - uses: actions/upload-artifact@v1 + - name: Package project + run: python3.8 release.py + - uses: actions/upload-artifact@v2 with: - name: cabal-linux-x86_64.xz - path: cabal-artifact.xz + name: cabal-linux + path: _build/artifacts/* artifact-macos: name: Artifact on macOS @@ -72,24 +66,12 @@ jobs: - name: Update Hackage index run: cabal v2-update - uses: actions/checkout@v2 - - name: Release project - run: | - cp cabal.project.release cabal.project - rm -rf cabal.project.local cabal.project.freeze - - name: Build - run: | - cabal v2-build cabal-install:exe:cabal - # macOS find doesn't know -executable - cp $(find dist-newstyle -type f -name cabal) cabal.exe - - name: Smoke test - run: | - ./cabal.exe --version - - name: Prepare for upload - run: xz -c < cabal.exe > cabal-artifact.xz - - uses: actions/upload-artifact@v1 + - name: Package project + run: python3 release.py + - uses: actions/upload-artifact@v2 with: - name: cabal-macos-x86_64.xz - path: cabal-artifact.xz + name: cabal-macos + path: _build/artifacts/* artifact-windows: name: Artifact on Windows @@ -114,24 +96,9 @@ jobs: - name: Update Hackage index run: cabal v2-update - uses: actions/checkout@v2 - - name: Release project - shell: bash - run: | - cp cabal.project.release cabal.project - rm -rf cabal.project.local cabal.project.freeze - - name: Build - shell: bash - run: | - cabal v2-build cabal-install:exe:cabal - cp dist-newstyle/build/x86_64-windows/ghc-8.6.5/cabal-install-3.4.0.0/x/cabal/build/cabal/cabal.exe cabal.exe - - name: Smoke test - shell: bash - run: | - ./cabal.exe --version - - name: Prepare for upload - shell: bash - run: xz -c < cabal.exe > cabal-artifact.xz - - uses: actions/upload-artifact@v1 + - name: Package project + run: python release.py + - uses: actions/upload-artifact@v2 with: - name: cabal-windows-x86_64.xz - path: cabal-artifact.xz + name: cabal-macos + path: _build/artifacts/* diff --git a/boot/ci-bootstrap.template.yml b/boot/ci-bootstrap.template.yml index 93e0c6b2087ff1dcf27889a93ecca3eee5da0f26..257177d9ad3d0286bfaa2af6ce7e002abb07110d 100644 --- a/boot/ci-bootstrap.template.yml +++ b/boot/ci-bootstrap.template.yml @@ -27,9 +27,15 @@ jobs: - name: bootstrap.py run: | python3 bootstrap/bootstrap.py -w /opt/ghc/8.6.5/bin/ghc -d bootstrap/linux-8.6.5.json + - name: Smoke test run: | - packages/tmp/bin/cabal --version + _build/bin/cabal --version + + - uses: actions/upload-artifact@v2 + with: + name: cabal-linux-bootstrapped + path: _build/artifacts/* boostrap-macos: name: Bootstrap on macOS @@ -52,4 +58,9 @@ jobs: - name: Smoke test run: | - packages/tmp/bin/cabal --version + _build/bin/cabal --version + + - uses: actions/upload-artifact@v2 + with: + name: cabal-macos-bootstrapped + path: _build/artifacts/* diff --git a/bootstrap/bootstrap.py b/bootstrap/bootstrap.py index 1d900af488e9a95f1ddd9482384b821eab67ba9b..832f526f84149e51842541b80f3619fd3dcf971f 100755 --- a/bootstrap/bootstrap.py +++ b/bootstrap/bootstrap.py @@ -10,7 +10,7 @@ See bootstrap/README.md for usage instructions. USAGE = """ This utility is only intended for use in building cabal-install 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 .`. or `release.py` """ from enum import Enum @@ -18,6 +18,7 @@ import hashlib import logging import json from pathlib import Path +import platform import shutil import subprocess from textwrap import dedent @@ -26,8 +27,16 @@ from typing import Set, Optional, Dict, List, Tuple, \ #logging.basicConfig(level=logging.INFO) -PACKAGES = Path('packages') -PKG_DB = PACKAGES / 'packages.conf' +BUILDDIR = Path('_build') + +BINDIR = BUILDDIR / 'bin' # binaries go there (--bindir) +DISTDIR = BUILDDIR / 'dists' # --builddir +UNPACKED = BUILDDIR / 'unpacked' # where we unpack tarballs +TARBALLS = BUILDDIR / 'tarballs' # where we download tarballks +PSEUDOSTORE = BUILDDIR / 'pseudostore' # where we install packages +ARTIFACTS = BUILDDIR / 'artifacts' # Where we put the archive +TMPDIR = BUILDDIR / 'tmp' # +PKG_DB = BUILDDIR / 'packages.conf' # package db PackageName = NewType('PackageName', str) Version = NewType('Version', str) @@ -99,10 +108,10 @@ class BadTarball(Exception): ]) def package_url(package: PackageName, version: Version) -> str: - return f'https://hackage.haskell.org/package/{package}-{version}/{package}-{version}.tar.gz' + return f'http://hackage.haskell.org/package/{package}-{version}/{package}-{version}.tar.gz' def package_cabal_url(package: PackageName, version: Version, revision: int) -> str: - return f'https://hackage.haskell.org/package/{package}-{version}/revision/{revision}.cabal' + return f'http://hackage.haskell.org/package/{package}-{version}/revision/{revision}.cabal' def verify_sha256(expected_hash: SHA256Hash, f: Path): h = hash_file(hashlib.sha256(), f.open('rb')) @@ -114,20 +123,22 @@ def fetch_package(package: PackageName, src_sha256: SHA256Hash, revision: Optional[int], cabal_sha256: Optional[SHA256Hash], - ) -> Path: + ) -> (Path, Path): import urllib.request # Download source distribution - out = PACKAGES / (f'{package}-{version}.tar.gz') - if not out.exists(): + tarball = TARBALLS / f'{package}-{version}.tar.gz' + if not tarball.exists(): print(f'Fetching {package}-{version}...') - out.parent.mkdir(parents=True, exist_ok=True) + tarball.parent.mkdir(parents=True, exist_ok=True) url = package_url(package, version) with urllib.request.urlopen(url) as resp: - shutil.copyfileobj(resp, out.open('wb')) + shutil.copyfileobj(resp, tarball.open('wb')) + + verify_sha256(src_sha256, tarball) # Download revised cabal file - cabal_file = PACKAGES / f'{package}.cabal' + 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) @@ -135,8 +146,7 @@ def fetch_package(package: PackageName, shutil.copyfileobj(resp, cabal_file.open('wb')) verify_sha256(cabal_sha256, cabal_file) - verify_sha256(src_sha256, out) - return out + return (tarball, cabal_file) def read_bootstrap_info(path: Path) -> BootstrapInfo: obj = json.load(path.open()) @@ -160,18 +170,19 @@ def check_builtin(dep: BuiltinDep, ghc: Compiler) -> None: return def install_dep(dep: BootstrapDep, ghc: Compiler) -> None: + dist_dir = (DISTDIR / f'{dep.package}-{dep.version}').resolve() + if dep.source == PackageSource.HACKAGE: assert dep.src_sha256 is not None - tarball = fetch_package(dep.package, dep.version, dep.src_sha256, + (tarball, cabal_file) = fetch_package(dep.package, dep.version, dep.src_sha256, dep.revision, dep.cabal_sha256) - subprocess_run(['tar', 'zxf', tarball.resolve()], - cwd=PACKAGES, check=True) - sdist_dir = PACKAGES / f'{dep.package}-{dep.version}' + 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 if dep.revision is not None: - shutil.copyfile(PACKAGES / f'{dep.package}.cabal', - sdist_dir / f'{dep.package}.cabal') + shutil.copyfile(cabal_file, sdist_dir / f'{dep.package}.cabal') elif dep.source == PackageSource.LOCAL: if dep.package == 'Cabal': @@ -181,28 +192,39 @@ def install_dep(dep: BootstrapDep, ghc: Compiler) -> None: else: raise ValueError(f'Unknown local package {dep.package}') - install_sdist(sdist_dir, ghc, dep.flags) + install_sdist(dist_dir, sdist_dir, ghc, dep.flags) -def install_sdist(sdist_dir: Path, ghc: Compiler, flags: List[str]): - prefix = (PACKAGES / 'tmp').resolve() +def install_sdist(dist_dir: Path, sdist_dir: Path, ghc: Compiler, flags: List[str]): + prefix = PSEUDOSTORE.resolve() flags_option = ' '.join(flags) + setup_dist_dir = dist_dir / 'setup' + setup = setup_dist_dir / 'Setup' - configure_args = [ + build_args = [ + f'--builddir={dist_dir}', + ] + + configure_args = build_args + [ f'--package-db={PKG_DB.resolve()}', f'--prefix={prefix}', + f'--bindir={BINDIR.resolve()}', f'--with-compiler={ghc.ghc_path}', f'--with-hc-pkg={ghc.ghc_pkg_path}', f'--with-hsc2hs={ghc.hsc2hs_path}', - f'--flags={flags_option}' + f'--flags={flags_option}', ] def check_call(args: List[str]) -> None: subprocess_run(args, cwd=sdist_dir, check=True) - check_call([str(ghc.ghc_path), '--make', '-package-env', '-', 'Setup']) - check_call(['./Setup', 'configure'] + configure_args) - check_call(['./Setup', 'build']) - check_call(['./Setup', 'install']) + setup_dist_dir.mkdir(parents=True, exist_ok=True) + + # Note: we pass -i so GHC doesn't look for anything else + # This should be fine for cabal-install dependencies. + check_call([str(ghc.ghc_path), '--make', '-package-env=-', '-i', f'-odir={setup_dist_dir}', f'-hidir={setup_dist_dir}', '-o', setup, 'Setup']) + check_call([setup, 'configure'] + configure_args) + check_call([setup, 'build'] + build_args) + check_call([setup, 'install'] + build_args) def hash_file(h, f: BinaryIO) -> SHA256Hash: while True: @@ -217,14 +239,6 @@ def hash_file(h, f: BinaryIO) -> SHA256Hash: UnitId = NewType('UnitId', str) PlanUnit = NewType('PlanUnit', dict) -def read_plan(project_dir: Path) -> Dict[UnitId, PlanUnit]: - path = project_dir / 'dist-newstyle' / 'cache' / 'plan.json' - plan = json.load(path.open('rb')) - return { - UnitId(c['id']): PlanUnit(c) - for c in plan['install-plan'] - } - def bootstrap(info: BootstrapInfo, ghc: Compiler) -> None: if not PKG_DB.exists(): print(f'Creating package database {PKG_DB}') @@ -237,6 +251,79 @@ def bootstrap(info: BootstrapInfo, ghc: Compiler) -> None: for dep in info.dependencies: install_dep(dep, ghc) +# Steps +####################################################################### + +def linuxname(i, r): + i = i.strip() # id + r = r.strip() # release + if i == '': return 'linux' + else: return f"{i}-{r}".lower() + +def macname(macver): + # https://en.wikipedia.org/wiki/MacOS_version_history#Releases + if macver.startswith('10.12.'): return 'sierra' + if macver.startswith('10.13.'): return 'high-sierra' + if macver.startswith('10.14.'): return 'mojave' + if macver.startswith('10.15.'): return 'catalina' + if macver.startswith('11.0.'): return 'big-sur' + else: return macver + +def archive_name(cabalversion): + # Ask platform information + machine = platform.machine() + if machine == '': machine = "unknown" + + system = platform.system().lower() + if system == '': system = "unknown" + + version = system + if system == 'linux': + try: + i = subprocess_run(['lsb_release', '-si'], stdout=subprocess.PIPE, encoding='UTF-8') + r = subprocess_run(['lsb_release', '-sr'], stdout=subprocess.PIPE, encoding='UTF-8') + version = linuxname(i.stdout, r.stdout) + except: + try: + with open('/etc/alpine-release') as f: + alpinever = f.read().strip() + return f'alpine-{alpinever}' + except: + pass + elif system == 'darwin': + version = 'darwin-' + macname(platform.mac_ver()[0]) + elif system == 'freebsd': + version = 'freebsd-' + platform.release().lower() + + return f'cabal-install-{cabalversion}-{machine}-{version}' + +def make_archive(cabal_path): + import tempfile + + print(f'Creating distribution tarball') + + # Get bootstrapped cabal version + # This also acts as smoke test + p = subprocess_run([cabal_path, '--numeric-version'], stdout=subprocess.PIPE, check=True, encoding='UTF-8') + cabalversion = p.stdout.replace('\n', '').strip() + + # Archive name + basename = ARTIFACTS.resolve() / (archive_name(cabalversion) + '-bootstrapped') + + # 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)) + shutil.copy(cabal_path, rootdir / 'cabal') + + # Make archive... + fmt = 'xztar' + if platform.system() == 'Windows': fmt = 'zip' + archivename = shutil.make_archive(basename, fmt, rootdir) + + return archivename + def main() -> None: import argparse parser = argparse.ArgumentParser( @@ -268,7 +355,9 @@ def main() -> None: info = read_bootstrap_info(args.deps) bootstrap(info, ghc) - cabal_path = (PACKAGES / 'tmp' / 'bin' / 'cabal').resolve() + cabal_path = (BINDIR / 'cabal').resolve() + + archive = make_archive(cabal_path) print(dedent(f''' Bootstrapping finished! @@ -277,6 +366,10 @@ def main() -> None: {cabal_path} + It have been archived for distribution in + + {archive} + You now should use this to build a full cabal-install distribution using v2-build. ''')) diff --git a/cabal-install/cabal-install.cabal b/cabal-install/cabal-install.cabal index 07d37b91b83281eb141639403b1b905bbd1bcaf3..4513feb98440a2c133f27becea3113d1b703281b 100644 --- a/cabal-install/cabal-install.cabal +++ b/cabal-install/cabal-install.cabal @@ -20,7 +20,7 @@ Copyright: 2003-2020, Cabal Development Team Category: Distribution Build-type: Simple Extra-Source-Files: - README.md bash-completion/cabal bootstrap.sh changelog + README.md bash-completion/cabal changelog -- Generated with 'make gen-extra-source-files' -- Do NOT edit this section manually; instead, run the script. diff --git a/cabal-install/cabal-install.cabal.dev b/cabal-install/cabal-install.cabal.dev index d4b8b81c7c0aaba26fec0b61eb2030db7bd55cf4..29c2b12b8481d0b6896de9209fcf479ed37354e2 100644 --- a/cabal-install/cabal-install.cabal.dev +++ b/cabal-install/cabal-install.cabal.dev @@ -20,7 +20,7 @@ Copyright: 2003-2020, Cabal Development Team Category: Distribution Build-type: Simple Extra-Source-Files: - README.md bash-completion/cabal bootstrap.sh changelog + README.md bash-completion/cabal changelog -- Generated with 'make gen-extra-source-files' -- Do NOT edit this section manually; instead, run the script. diff --git a/cabal-install/cabal-install.cabal.prod b/cabal-install/cabal-install.cabal.prod index 07d37b91b83281eb141639403b1b905bbd1bcaf3..4513feb98440a2c133f27becea3113d1b703281b 100644 --- a/cabal-install/cabal-install.cabal.prod +++ b/cabal-install/cabal-install.cabal.prod @@ -20,7 +20,7 @@ Copyright: 2003-2020, Cabal Development Team Category: Distribution Build-type: Simple Extra-Source-Files: - README.md bash-completion/cabal bootstrap.sh changelog + README.md bash-completion/cabal changelog -- Generated with 'make gen-extra-source-files' -- Do NOT edit this section manually; instead, run the script. diff --git a/cabal-install/cabal-install.cabal.zinza b/cabal-install/cabal-install.cabal.zinza index 62da8d13643152d52e61e8ddc668822aeb3d01ce..2b412d34337baaefef3dab0ddb6164491bb25b0e 100644 --- a/cabal-install/cabal-install.cabal.zinza +++ b/cabal-install/cabal-install.cabal.zinza @@ -303,7 +303,7 @@ Copyright: 2003-2020, Cabal Development Team Category: Distribution Build-type: Simple Extra-Source-Files: - README.md bash-completion/cabal bootstrap.sh changelog + README.md bash-completion/cabal changelog -- Generated with 'make gen-extra-source-files' -- Do NOT edit this section manually; instead, run the script. diff --git a/release.py b/release.py new file mode 100755 index 0000000000000000000000000000000000000000..93ab91cb4ee2b64abe77fa73c6b9edc04d5b94bb --- /dev/null +++ b/release.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +release.py - build the release of cabal-install" +""" + +USAGE = """ +This utility is only intended for use in building cabal-install +binary distributions on platforms with existing cabal-install. +""" + +# TODO, by using v2-install we build from sdists, which is good +# But we cannot get plan.json, to get dependency-receipt +# https://github.com/haskell/cabal/issues/6988 + +# TODO provide DWARF enabled builds? + +# We don't build documentation, its well built by readthedocs. +# We cannot make tarball, as the private key for signing should be on the builder machine. +# We also don't use caching, this way we have one moving part less. + +import os +import platform +import shutil +import subprocess + +from pathlib import Path +from textwrap import dedent +from typing import NamedTuple + +DEFAULT_INDEXSTATE='2020-07-23T11:14:13Z' + +Args = NamedTuple('Args', [ + ('compiler', Path), + ('cabal', Path), + ('indexstate', str), + ('rootdir', Path), + ('builddir', Path), + ('static', bool), + ('ofdlocking', bool), +]) + +# utils +####################################################################### + +def subprocess_run(args, **kwargs): + "Like subprocess.run, but also print what we run" + + args = list(map(str, args)) # For Windows, https://www.scivision.dev/windows-python-pathlib-subprocess-bug/ + args_str = ' '.join(map(str, args)) + extras = '' + if 'cwd' in kwargs: + extras += f' cwd={kwargs["cwd"]}' + print(f'%{extras} {args_str}') + + return subprocess.run(args, **kwargs) + +# archive name +####################################################################### + +def linuxname(i, r): + i = i.strip() # id + r = r.strip() # release + if i == '': return 'linux' + else: return f"{i}-{r}".lower() + +def macname(macver): + # https://en.wikipedia.org/wiki/MacOS_version_history#Releases + if macver.startswith('10.12.'): return 'sierra' + if macver.startswith('10.13.'): return 'high-sierra' + if macver.startswith('10.14.'): return 'mojave' + if macver.startswith('10.15.'): return 'catalina' + if macver.startswith('11.0.'): return 'big-sur' + else: return macver + +def archive_name(cabalversion): + # Ask platform information + machine = platform.machine() + if machine == '': machine = "unknown" + + system = platform.system().lower() + if system == '': system = "unknown" + + version = system + if system == 'linux': + try: + i = subprocess_run(['lsb_release', '-si'], stdout=subprocess.PIPE, encoding='UTF-8') + r = subprocess_run(['lsb_release', '-sr'], stdout=subprocess.PIPE, encoding='UTF-8') + version = linuxname(i.stdout, r.stdout) + except: + try: + with open('/etc/alpine-release') as f: + alpinever = f.read().strip() + version = f'alpine-{alpinever}' + except: + pass + elif system == 'darwin': + version = 'darwin-' + macname(platform.mac_ver()[0]) + elif system == 'freebsd': + version = 'freebsd-' + platform.release().lower() + + return f'cabal-install-{cabalversion}-{machine}-{version}' + +# Steps +####################################################################### + +def step_makedirs(args: Args): + (args.builddir / 'bin').mkdir(parents=True, exist_ok=True) + (args.builddir / 'cabal').mkdir(parents=True, exist_ok=True) + +# 57936384 +def step_config(args: Args): + splitsections = '' + if platform.system() == 'Linux': + splitsections = 'split-sections: True' + + # https://github.com/Mistuke/CabalChoco/blob/d0e1d2fd8ce13ab4271c4b906ca0bde3b710a310/3.2.0.0/cabal/tools/chocolateyInstall.ps1#L289 + extraprogpath = str(args.builddir / 'bin') + if platform.system() == 'Windows': + msysbin = Path('C:\\tools\\msys64\\usr\\bin') + if msysbin.is_dir(): + extraprogpath = extraprogpath + ";" + str(msysbin) + + config = dedent(f""" + repository hackage.haskell.org + url: http://hackage.haskell.org/ + + remote-build-reporting: anonymous + remote-repo-cache: {args.builddir}/cabal/packages + + write-ghc-environment-files: never + install-method: copy + overwrite-policy: always + + documentation: False + {splitsections} + + build-summary: {args.builddir}/cabal/logs/build.log + installdir: {args.builddir}/bin + logs-dir: {args.builddir}/cabal/logs + store-dir: {args.builddir}/cabal/store + symlink-bindir: {args.builddir}/bin + world-file: {args.builddir}/cabal/world + extra-prog-path: {extraprogpath} + + jobs: 1 + + install-dirs user + prefix: {args.builddir} + """) + + with open(args.builddir / 'cabal' / 'config', 'w') as f: + f.write(config) + + cabal_project_local ='' + if args.static: + # --enable-executable-static doesn't affect "non local" executables, as in v2-install project + cabal_project_local += dedent(""" + package cabal-install + executable-static: True + """) + cabal_project_local += dedent(f""" + package lukko + flags: {'+' if args.ofdlocking else '-'}ofd_locking + """) + + with open(args.rootdir / 'cabal.project.release.local', 'w') as f: + f.write(cabal_project_local) + +def make_env(args: Args): + env = { + 'PATH': os.environ['PATH'], + 'CABAL_DIR': str(args.builddir), + 'CABAL_CONFIG': str(args.builddir / 'cabal' / 'config'), + } + # https://superuser.com/questions/1079017/is-there-an-environment-variable-for-c-users-username-appdata-local-temp-in-w + # In particular, we surely need 'TEMP' + # And also SYSTEMROOT to make 'curl' work! + envvars = [ + 'LANG', + 'HOME', 'HOMEDRIVE', 'HOMEPATH', + 'TMP', 'TEMP', + 'PATHEXT', 'APPDATA', 'LOCALAPPDATA', 'SYSTEMROOT', + ] + for key in envvars: + if key in os.environ: + env[key] = os.environ[key] + + return env + +def step_cabal_update(args: Args): + env = make_env(args) + subprocess_run([ + args.cabal, + 'v2-update', + '-v', + f'--index-state={args.indexstate}', + ], check=True, env=env) + +def step_cabal_install(args: Args): + env = make_env(args) + subprocess_run([ + args.cabal, + 'v2-install', + '-v', + 'cabal-install:exe:cabal', + '--project-file=cabal.project.release', + f'--with-compiler={args.compiler}', + ], check=True, env=env) + +def step_make_archive(args: Args): + import tempfile + + print(f'Creating distribution tarball') + + # Get bootstrapped cabal version + # This also acts as smoke test + cabal_path = args.builddir / 'bin' / 'cabal' + if platform.system() == 'Windows': + cabal_path = cabal_path.with_suffix('.exe') + p = subprocess_run([cabal_path, '--numeric-version'], stdout=subprocess.PIPE, check=True, encoding='UTF-8') + cabalversion = p.stdout.replace('\n', '').strip() + + # Archive name + name = archive_name(cabalversion) + if args.static: + name = name + "-static" + if not args.ofdlocking: + name = name + "-noofd" + + basename = args.builddir / 'artifacts' / name + + # In temporary directory, create a directory which we will archive + tmpdir = args.builddir / 'tmp' + tmpdir.mkdir(parents=True, exist_ok=True) + + rootdir = Path(tempfile.mkdtemp(dir=tmpdir)) + shutil.copy(cabal_path, rootdir / 'cabal') + + # Make archive... + fmt = 'xztar' + if platform.system() == 'Windows': fmt = 'zip' + archivename = shutil.make_archive(basename, fmt, rootdir) + + return archivename + +# Main procedure +####################################################################### + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="release packaging utility for cabal-install.", + epilog = USAGE, + formatter_class = argparse.RawDescriptionHelpFormatter) + + class EnableDisable(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + value = option_string.startswith('--enable') + setattr(namespace, self.dest, value) + + parser.add_argument('-w', '--with-compiler', type=str, default='ghc', help='path to GHC') + parser.add_argument('-C', '--with-cabal', type=str, default='cabal', help='path to cabal-install') + parser.add_argument('-i', '--index-state', type=str, default=DEFAULT_INDEXSTATE, help='index state of Hackage to use') + parser.add_argument('--enable-static-executable', '--disable-static-executable', dest='static', nargs=0, default=False, action=EnableDisable, help='Statically link cabal executable') + parser.add_argument('--enable-ofd-locking', '--disable-ofd-locking', dest='ofd_locking', nargs=0, default=True, action=EnableDisable, help='OFD locking (lukko)') + + args = parser.parse_args() + + rootdir = Path('.').resolve() + args = Args( + compiler = Path(shutil.which(args.with_compiler)), + cabal = Path(shutil.which(args.with_cabal)), + indexstate = args.index_state, + rootdir = rootdir, + builddir = rootdir.resolve() / '_build', + static = args.static, + ofdlocking = args.ofd_locking + ) + + print(dedent(f""" + compiler: {args.compiler} + cabal: {args.cabal} + index-state: {args.indexstate} + builddir: {args.builddir} + static: {args.static} + ofd-locking: {args.ofdlocking} + """)) + + # Check tools + subprocess_run([args.compiler, '--version'], check=True) + subprocess_run([args.compiler, '--print-project-git-commit-id'], check=True) + subprocess_run([args.cabal, '--version'], check=True) + + step_makedirs(args) + step_config(args) + step_cabal_update(args) + step_cabal_install(args) + archivename = step_make_archive(args) + + print(dedent(f''' + Packaging finished! + + Distribution have been archived in + + {archivename} + + ''')) + +if __name__ == '__main__': + main()