Repository navigation
Expand file tree
/
Copy pathcli.py
More file actions
208 lines (164 loc) · 7.31 KB
/
Copy pathcli.py
File metadata and controls
208 lines (164 loc) · 7.31 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
207
208
import logging
import click
import runez
from runez.pyenv import PythonSpec
from runez.render import PrettyTable
from portable_python import BuildSetup, PPG
from portable_python.inspector import LibAutoCorrect, PythonInspector
LOG = logging.getLogger(__name__)
@runez.click.group()
@runez.click.version()
@runez.click.color()
@runez.click.debug("-v")
@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("--target", "-t", hidden=True, help="For internal use / testing")
def main(debug, config, target):
"""
Build (optionally portable) python binaries
"""
runez.system.AbortException = SystemExit
runez.log.setup(
debug=debug,
console_format="%(levelname)s %(message)s",
console_level=logging.INFO,
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(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(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, fatal=False)
if not fam:
runez.abort("Python family '%s' is not yet supported" % runez.red(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:
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)
folders = PPG.get_folders(base=".", family=pspec and pspec.family, version=pspec and pspec.version)
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__":
from portable_python.cli import main
main()