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()