Repository navigation
Expand file tree
/
Copy pathcli.py
More file actions
206 lines (164 loc) · 7.55 KB
/
Copy pathcli.py
File metadata and controls
206 lines (164 loc) · 7.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import logging
import click
import runez
from runez.pyenv import PythonSpec
from runez.render import PrettyTable
from portable_python import BuildSetup, PPG, required_python_spec
from portable_python.inspector import LibAutoCorrect, PythonInspector
LOG = logging.getLogger(__name__)
@runez.click.group()
@runez.click.version()
@runez.click.color()
@runez.click.dryrun("-n")
@click.option("--config", "-c", metavar="PATH", default="portable-python.yml", show_default=True, help="Path to config file to use")
@click.option("--quiet", "-q", is_flag=True, help="Turn off DEBUG logging")
@click.option("--target", "-t", hidden=True, help="For internal use / testing")
def main(config, quiet, target):
"""
Build (optionally portable) python binaries
"""
level = logging.INFO if quiet else logging.DEBUG
runez.system.AbortException = SystemExit
runez.log.setup(
debug=not quiet,
level=level,
console_format="%(levelname)s %(message)s",
console_level=level,
default_logger=LOG.info,
locations=None,
)
PPG.grab_config(config, target=target)
@main.command()
@click.option("--modules", "-m", metavar="CSV", help="External modules to include")
@click.option("--prefix", "-p", metavar="PATH", help="Use given --prefix for python installation (not portable)")
@click.argument("python_spec")
def build(modules, prefix, python_spec):
"""Build a portable python binary"""
setup = BuildSetup(required_python_spec(python_spec), modules=modules, prefix=prefix)
setup.compile()
@main.command()
@click.option("--modules", "-m", metavar="CSV", help="External modules to include")
@click.argument("python_spec", required=False)
def build_report(modules, python_spec):
"""Show status of buildable modules, which will be auto-compiled"""
setup = BuildSetup(required_python_spec(python_spec), modules=modules)
print(runez.bold(setup.python_spec))
report = setup.python_builder.modules.report()
print(report)
setup.validate_module_selection()
@main.command()
def diagnostics():
"""Show diagnostics info"""
with runez.Anchored("."):
config = PPG.config.represented()
print(PrettyTable.two_column_diagnostics(_diagnostics(), config))
@main.command()
@click.option("--modules", "-m", help="Modules to inspect")
@click.option("--verbose", "-v", is_flag=True, help="Show full so report")
@click.option("--prefix", "-p", is_flag=True, help="Build was done with --prefix (not portable)")
@click.option("--skip-so", "-s", is_flag=True, help="Don't check all .so-s")
@click.argument("path")
def inspect(modules, verbose, prefix, skip_so, path):
"""Inspect a python installation for non-portable dynamic lib usage"""
if path != "invoker":
path = runez.resolved_path(path)
inspector = PythonInspector(path, modules=modules)
runez.abort_if(inspector.python.problem, "%s: %s" % (runez.red(path), inspector.python.problem))
print(runez.blue(inspector.python))
print(inspector.represented(verbose=verbose))
if not skip_so and (not modules or modules == "all"):
problem = inspector.full_so_report.get_problem(portable=not prefix)
runez.abort_if(problem)
@main.command(name="list")
@click.option("--json", is_flag=True, help="Json output")
@click.argument("family", default="cpython")
def list_cmd(json, family):
"""List latest versions"""
fam = PPG.family(family)
if json:
print(runez.represented_json(fam.available_versions))
return
print("%s:" % runez.bold(family))
for mm, v in fam.available_versions.items():
print(" %s: %s" % (runez.bold(mm), v))
def _diagnostics():
yield "invoker python", runez.SYS_INFO.invoker_python
yield from runez.SYS_INFO.diagnostics()
def _find_recompress_source(folders, path):
candidate = runez.to_path(path)
if candidate.exists() or candidate.is_absolute():
return candidate.absolute() if candidate.exists() else None
candidates = [folders.base_folder, folders.build_folder, folders.dist, folders.destdir]
for candidate in candidates:
if candidate:
# TODO: `runez.to_path()` can be simplified away when all `folders.` attributes become of type Path
candidate = runez.to_path(candidate) / path
if candidate.exists():
return candidate.absolute()
@runez.log.timeit
def recompress_folder(folders, path, extension):
"""Recompress folder"""
dest = runez.SYS_INFO.platform_id.composed_basename("cpython", path.name, extension=extension)
dest = folders.dist / dest
runez.compress(path, dest, logger=print)
return dest
@runez.log.timeit
def recompress_archive(folders, path, extension):
stem = path.name.rpartition(".")[0]
if stem.endswith(".tar"):
stem = stem.rpartition(".")[0]
dest = "%s.%s" % (stem, extension)
dest = folders.dist / dest
if dest == path:
dest = "%s.%s" % (stem + "-recompressed", extension)
dest = folders.dist / dest
with runez.TempFolder() as _:
tmp_folder = runez.to_path("tmp")
runez.decompress(path, tmp_folder, simplify=True, logger=print)
runez.compress(tmp_folder, dest.name, arcname=dest.name, logger=print)
runez.move(dest.name, dest, logger=print)
return dest
@main.command()
@click.argument("path", required=True)
@click.argument("ext", required=True, type=click.Choice(runez.SYS_INFO.platform_id.supported_compression))
def recompress(path, ext):
"""
Re-compress an existing binary tarball, or folder
\b
Mildly useful for comparing sizes from different compressions
""" # noqa: D301
extension = runez.SYS_INFO.platform_id.canonical_compress_extension(ext)
pspec = PythonSpec.from_text(path) # Best effort: a build folder like `3.9.7` yields a spec, archive names don't
folders = PPG.get_folders(base=".", family=pspec.family if pspec else "cpython", version=pspec.version if pspec else None)
with runez.Anchored(folders.base_folder):
actual_path = _find_recompress_source(folders, path)
if not actual_path:
runez.abort("'%s' does not exist" % runez.red(runez.short(path)))
if actual_path.is_dir():
dest = recompress_folder(folders, actual_path, extension)
else:
dest = recompress_archive(folders, actual_path, extension)
print("Size of %s: %s" % (runez.short(actual_path), runez.bold(runez.represented_bytesize(actual_path))))
print("Size of %s: %s" % (runez.short(dest), runez.bold(runez.represented_bytesize(dest))))
@main.command()
@click.option("--commit", is_flag=True, help="Effectively perform the changes")
@click.option("--prefix", "-p", metavar="PATH", help="--prefix the program was built with (default: same as scanned path)")
@click.argument("path", required=True)
def lib_auto_correct(commit, prefix, path):
"""
Scan a python installation, auto-correct exes/libraries to use relative paths
This is mostly for testing purposes, applies the same method done internally by this tool.
Allows to exercise just the lib-auto-correct part without having to wait for full build to complete.
"""
if not runez.DRYRUN:
runez.log.set_dryrun(not commit)
path = runez.resolved_path(path)
if not prefix:
python = PPG.find_python(path)
runez.abort_if(python.problem)
r = runez.run(python.executable, "-c", "import sysconfig; print(sysconfig.get_config_var('prefix'))", dryrun=False)
prefix = runez.resolved_path(r.output)
lib_auto_correct = LibAutoCorrect(prefix, runez.to_path(path))
lib_auto_correct.run()
if __name__ == "__main__":
main()