2 # -*- coding: utf-8 -*-
5 bootstrap.py - bootstrapping utility for cabal-install.
7 See bootstrap/README.md for usage instructions.
11 This utility is only intended for use in building cabal-install
12 on a new platform. If you already have a functional (if dated) cabal-install
13 please rather run `cabal v2-install .`.
20 from pathlib
import Path
24 from textwrap
import dedent
25 from typing
import Set
, Optional
, Dict
, List
, Tuple
, \
26 NewType
, BinaryIO
, NamedTuple
, TypeVar
28 #logging.basicConfig(level=logging.INFO)
30 BUILDDIR
= Path('_build')
32 BINDIR
= BUILDDIR
/ 'bin' # binaries go there (--bindir)
33 DISTDIR
= BUILDDIR
/ 'dists' # --builddir
34 UNPACKED
= BUILDDIR
/ 'unpacked' # where we unpack tarballs
35 TARBALLS
= BUILDDIR
/ 'tarballs' # where we download tarballks
36 PSEUDOSTORE
= BUILDDIR
/ 'pseudostore' # where we install packages
37 ARTIFACTS
= BUILDDIR
/ 'artifacts' # Where we put the archive
38 TMPDIR
= BUILDDIR
/ 'tmp' #
39 PKG_DB
= BUILDDIR
/ 'packages.conf' # package db
41 PackageName
= NewType('PackageName', str)
42 Version
= NewType('Version', str)
43 SHA256Hash
= NewType('SHA256Hash', str)
45 class PackageSource(Enum
):
49 BuiltinDep
= NamedTuple('BuiltinDep', [
50 ('package', PackageName
),
54 BootstrapDep
= NamedTuple('BootstrapDep', [
55 ('package', PackageName
),
57 ('source', PackageSource
),
58 # source tarball SHA256
59 ('src_sha256', Optional
[SHA256Hash
]),
60 # `revision` is only valid when source == HACKAGE.
61 ('revision', Optional
[int]),
62 ('cabal_sha256', Optional
[SHA256Hash
]),
66 BootstrapInfo
= NamedTuple('BootstrapInfo', [
67 ('builtin', List
[BuiltinDep
]),
68 ('dependencies', List
[BootstrapDep
]),
72 def __init__(self
, ghc_path
: Path
):
73 if not ghc_path
.is_file():
74 raise TypeError(f
'GHC {ghc_path} is not a file')
76 self
.ghc_path
= ghc_path
.resolve()
78 info
= self
._get
_ghc
_info
()
79 self
.version
= info
['Project version']
80 #self.lib_dir = Path(info['LibDir'])
81 #self.ghc_pkg_path = (self.lib_dir / 'bin' / 'ghc-pkg').resolve()
82 self
.ghc_pkg_path
= (self
.ghc_path
.parent
/ 'ghc-pkg').resolve()
83 if not self
.ghc_pkg_path
.is_file():
84 raise TypeError(f
'ghc-pkg {self.ghc_pkg_path} is not a file')
85 self
.hsc2hs_path
= (self
.ghc_path
.parent
/ 'hsc2hs').resolve()
86 if not self
.hsc2hs_path
.is_file():
87 raise TypeError(f
'hsc2hs {self.hsc2hs_path} is not a file')
89 def _get_ghc_info(self
) -> Dict
[str,str]:
90 from ast
import literal_eval
91 p
= subprocess_run([self
.ghc_path
, '--info'], stdout
=subprocess
.PIPE
, check
=True, encoding
='UTF-8')
92 out
= p
.stdout
.replace('\n', '').strip()
93 return dict(literal_eval(out
))
95 PackageSpec
= Tuple
[PackageName
, Version
]
97 class BadTarball(Exception):
98 def __init__(self
, path
: Path
, expected_sha256
: SHA256Hash
, found_sha256
: SHA256Hash
):
100 self
.expected_sha256
= expected_sha256
101 self
.found_sha256
= found_sha256
105 f
'Bad tarball hash: {str(self.path)}',
106 f
' expected: {self.expected_sha256}',
107 f
' found: {self.found_sha256}',
110 def package_url(package
: PackageName
, version
: Version
) -> str:
111 return f
'http://hackage.haskell.org/package/{package}-{version}/{package}-{version}.tar.gz'
113 def package_cabal_url(package
: PackageName
, version
: Version
, revision
: int) -> str:
114 return f
'http://hackage.haskell.org/package/{package}-{version}/revision/{revision}.cabal'
116 def verify_sha256(expected_hash
: SHA256Hash
, f
: Path
):
117 h
= hash_file(hashlib
.sha256(), f
.open('rb'))
118 if h
!= expected_hash
:
119 raise BadTarball(f
, expected_hash
, h
)
121 def fetch_package(package
: PackageName
,
123 src_sha256
: SHA256Hash
,
124 revision
: Optional
[int],
125 cabal_sha256
: Optional
[SHA256Hash
],
127 import urllib
.request
129 # Download source distribution
130 tarball
= TARBALLS
/ f
'{package}-{version}.tar.gz'
131 if not tarball
.exists():
132 print(f
'Fetching {package}-{version}...')
133 tarball
.parent
.mkdir(parents
=True, exist_ok
=True)
134 url
= package_url(package
, version
)
135 with urllib
.request
.urlopen(url
) as resp
:
136 shutil
.copyfileobj(resp
, tarball
.open('wb'))
138 verify_sha256(src_sha256
, tarball
)
140 # Download revised cabal file
141 cabal_file
= TARBALLS
/ f
'{package}.cabal'
142 if revision
is not None and not cabal_file
.exists():
143 assert cabal_sha256
is not None
144 url
= package_cabal_url(package
, version
, revision
)
145 with urllib
.request
.urlopen(url
) as resp
:
146 shutil
.copyfileobj(resp
, cabal_file
.open('wb'))
147 verify_sha256(cabal_sha256
, cabal_file
)
149 return (tarball
, cabal_file
)
151 def read_bootstrap_info(path
: Path
) -> BootstrapInfo
:
152 obj
= json
.load(path
.open())
154 def bi_from_json(o
: dict) -> BuiltinDep
:
155 return BuiltinDep(**o
)
157 def dep_from_json(o
: dict) -> BootstrapDep
:
158 o
['source'] = PackageSource(o
['source'])
159 return BootstrapDep(**o
)
161 builtin
= [bi_from_json(dep
) for dep
in obj
['builtin'] ]
162 deps
= [dep_from_json(dep
) for dep
in obj
['dependencies'] ]
164 return BootstrapInfo(dependencies
=deps
, builtin
=builtin
)
166 def check_builtin(dep
: BuiltinDep
, ghc
: Compiler
) -> None:
167 subprocess_run([str(ghc
.ghc_pkg_path
), 'describe', f
'{dep.package}-{dep.version}'],
168 check
=True, stdout
=subprocess
.DEVNULL
)
169 print(f
'Using {dep.package}-{dep.version} from GHC...')
172 def install_dep(dep
: BootstrapDep
, ghc
: Compiler
) -> None:
173 dist_dir
= (DISTDIR
/ f
'{dep.package}-{dep.version}').resolve()
175 if dep
.source
== PackageSource
.HACKAGE
:
176 assert dep
.src_sha256
is not None
177 (tarball
, cabal_file
) = fetch_package(dep
.package
, dep
.version
, dep
.src_sha256
,
178 dep
.revision
, dep
.cabal_sha256
)
179 UNPACKED
.mkdir(parents
=True, exist_ok
=True)
180 shutil
.unpack_archive(tarball
.resolve(), UNPACKED
, 'gztar')
181 sdist_dir
= UNPACKED
/ f
'{dep.package}-{dep.version}'
183 # Update cabal file with revision
184 if dep
.revision
is not None:
185 shutil
.copyfile(cabal_file
, sdist_dir
/ f
'{dep.package}.cabal')
187 elif dep
.source
== PackageSource
.LOCAL
:
188 if dep
.package
== 'Cabal':
189 sdist_dir
= Path('Cabal').resolve()
190 elif dep
.package
== 'Cabal-syntax':
191 sdist_dir
= Path('Cabal-syntax').resolve()
192 elif dep
.package
== 'cabal-install-solver':
193 sdist_dir
= Path('cabal-install-solver').resolve()
194 elif dep
.package
== 'cabal-install':
195 sdist_dir
= Path('cabal-install').resolve()
197 raise ValueError(f
'Unknown local package {dep.package}')
199 install_sdist(dist_dir
, sdist_dir
, ghc
, dep
.flags
)
201 def install_sdist(dist_dir
: Path
, sdist_dir
: Path
, ghc
: Compiler
, flags
: List
[str]):
202 prefix
= PSEUDOSTORE
.resolve()
203 flags_option
= ' '.join(flags
)
204 setup_dist_dir
= dist_dir
/ 'setup'
205 setup
= setup_dist_dir
/ 'Setup'
208 f
'--builddir={dist_dir}',
211 configure_args
= build_args
+ [
212 f
'--package-db={PKG_DB.resolve()}',
213 f
'--prefix={prefix}',
214 f
'--bindir={BINDIR.resolve()}',
215 f
'--with-compiler={ghc.ghc_path}',
216 f
'--with-hc-pkg={ghc.ghc_pkg_path}',
217 f
'--with-hsc2hs={ghc.hsc2hs_path}',
218 f
'--flags={flags_option}',
221 def check_call(args
: List
[str]) -> None:
222 subprocess_run(args
, cwd
=sdist_dir
, check
=True)
224 setup_dist_dir
.mkdir(parents
=True, exist_ok
=True)
226 # Note: we pass -i so GHC doesn't look for anything else
227 # This should be fine for cabal-install dependencies.
228 check_call([str(ghc
.ghc_path
), '--make', '-package-env=-', '-i', f
'-odir={setup_dist_dir}', f
'-hidir={setup_dist_dir}', '-o', setup
, 'Setup'])
229 check_call([setup
, 'configure'] + configure_args
)
230 check_call([setup
, 'build'] + build_args
)
231 check_call([setup
, 'install'] + build_args
)
233 def hash_file(h
, f
: BinaryIO
) -> SHA256Hash
:
237 return SHA256Hash(h
.hexdigest())
242 # Cabal plan.json representation
243 UnitId
= NewType('UnitId', str)
244 PlanUnit
= NewType('PlanUnit', dict)
246 def bootstrap(info
: BootstrapInfo
, ghc
: Compiler
) -> None:
247 if not PKG_DB
.exists():
248 print(f
'Creating package database {PKG_DB}')
249 PKG_DB
.parent
.mkdir(parents
=True, exist_ok
=True)
250 subprocess_run([ghc
.ghc_pkg_path
, 'init', PKG_DB
])
252 for dep
in info
.builtin
:
253 check_builtin(dep
, ghc
)
255 for dep
in info
.dependencies
:
256 install_dep(dep
, ghc
)
259 #######################################################################
263 r
= r
.strip() # release
264 if i
== '': return 'linux'
265 else: return f
"{i}-{r}".lower()
268 # https://en.wikipedia.org/wiki/MacOS_version_history#Releases
269 if macver
.startswith('10.12.'): return 'sierra'
270 if macver
.startswith('10.13.'): return 'high-sierra'
271 if macver
.startswith('10.14.'): return 'mojave'
272 if macver
.startswith('10.15.'): return 'catalina'
273 if macver
.startswith('11.0.'): return 'big-sur'
276 def archive_name(cabalversion
):
277 # Ask platform information
278 machine
= platform
.machine()
279 if machine
== '': machine
= "unknown"
281 system
= platform
.system().lower()
282 if system
== '': system
= "unknown"
285 if system
== 'linux':
287 i
= subprocess_run(['lsb_release', '-si'], stdout
=subprocess
.PIPE
, encoding
='UTF-8')
288 r
= subprocess_run(['lsb_release', '-sr'], stdout
=subprocess
.PIPE
, encoding
='UTF-8')
289 version
= linuxname(i
.stdout
, r
.stdout
)
292 with
open('/etc/alpine-release') as f
:
293 alpinever
= f
.read().strip()
294 return f
'alpine-{alpinever}'
297 elif system
== 'darwin':
298 version
= 'darwin-' + macname(platform
.mac_ver()[0])
299 elif system
== 'freebsd':
300 version
= 'freebsd-' + platform
.release().lower()
302 return f
'cabal-install-{cabalversion}-{machine}-{version}'
304 def make_archive(cabal_path
):
307 print(f
'Creating distribution tarball')
309 # Get bootstrapped cabal version
310 # This also acts as smoke test
311 p
= subprocess_run([cabal_path
, '--numeric-version'], stdout
=subprocess
.PIPE
, check
=True, encoding
='UTF-8')
312 cabalversion
= p
.stdout
.replace('\n', '').strip()
315 basename
= ARTIFACTS
.resolve() / (archive_name(cabalversion
) + '-bootstrapped')
317 # In temporary directory, create a directory which we will archive
318 tmpdir
= TMPDIR
.resolve()
319 tmpdir
.mkdir(parents
=True, exist_ok
=True)
321 rootdir
= Path(tempfile
.mkdtemp(dir=tmpdir
))
322 shutil
.copy(cabal_path
, rootdir
/ 'cabal')
326 if platform
.system() == 'Windows': fmt
= 'zip'
327 archivename
= shutil
.make_archive(basename
, fmt
, rootdir
)
333 parser
= argparse
.ArgumentParser(
334 description
="bootstrapping utility for cabal-install.",
336 formatter_class
= argparse
.RawDescriptionHelpFormatter
)
337 parser
.add_argument('-d', '--deps', type=Path
, default
='bootstrap-deps.json',
338 help='bootstrap dependency file')
339 parser
.add_argument('-w', '--with-compiler', type=Path
,
341 args
= parser
.parse_args()
344 if args
.with_compiler
is None:
345 path
= shutil
.which('ghc')
347 raise ValueError("Couldn't find ghc in PATH")
348 ghc
= Compiler(Path(path
))
350 ghc
= Compiler(args
.with_compiler
)
352 print(f
'Bootstrapping cabal-install with GHC {ghc.version} at {ghc.ghc_path}...')
355 DO NOT use this script if you have another recent cabal-install available.
356 This script is intended only for bootstrapping cabal-install on new
360 info
= read_bootstrap_info(args
.deps
)
362 cabal_path
= (BINDIR
/ 'cabal').resolve()
364 archive
= make_archive(cabal_path
)
367 Bootstrapping finished!
369 The resulting cabal-install executable can be found at
373 It have been archived for distribution in
377 You now should use this to build a full cabal-install distribution
381 def subprocess_run(args
, **kwargs
):
382 "Like subprocess.run, but also print what we run"
384 args_str
= ' '.join(map(str, args
))
387 extras
+= f
' cwd={kwargs["cwd"]}'
388 print(f
'bootstrap: running{extras} {args_str}')
390 return subprocess
.run(args
, **kwargs
)
392 if __name__
== '__main__':