Repository navigation
Expand file tree
/
Copy pathcli.py
More file actions
212 lines (166 loc) · 6.87 KB
/
Copy pathcli.py
File metadata and controls
212 lines (166 loc) · 6.87 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
209
210
211
212
import logging
import sys
import click
import runez
from runez.pyenv import PythonDepot
from runez.render import PrettyTable
from portable_python import BuildSetup, CLEANABLE_CHOICES, LOG
from portable_python.config import Config
from portable_python.inspector import PythonInspector
from portable_python.versions import PythonVersions
CONFIG_PATH = "portable-python.yml"
@runez.click.group()
@runez.click.version()
@runez.click.color()
@runez.click.debug("-v")
@runez.click.dryrun("-n")
@click.option("--config", "-c", metavar="PATH", default=CONFIG_PATH, show_default=True, help="Path to config file to use")
def main(debug, config):
"""
Build (optionally portable) python binaries
"""
global CONFIG_PATH
CONFIG_PATH = config
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,
)
@main.command()
@click.option("--clean", "-c", multiple=True, metavar="CSV", help="State what to cleanup, one of: %s" % CLEANABLE_CHOICES)
@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(clean, modules, prefix, python_spec):
"""Build a portable python binary"""
setup = BuildSetup(python_spec, CONFIG_PATH, modules=modules, prefix=prefix)
setup.set_requested_clean(clean)
setup.compile()
@main.command()
def diagnostics():
"""Show diagnostics info"""
depot = PythonDepot(use_path=True)
depot.scan_path_env_var()
def _diagnostics():
yield "invoker python", depot.invoker
yield from runez.SYS_INFO.diagnostics()
config = Config(CONFIG_PATH).represented()
print(PrettyTable.two_column_diagnostics(_diagnostics(), depot.representation(), config))
@main.command()
@click.option("--modules", "-m", help="Modules to inspect")
@click.option("--verbose", "-v", is_flag=True, multiple=True, default=None, help="Show full so report")
@click.option("--validate", is_flag=True, help="Exit with code 1 if anything looks incomplete")
@click.argument("pythons", nargs=-1)
def inspect(modules, verbose, validate, pythons):
"""Inspect a python installation for non-portable dynamic lib usage"""
verbose = len(verbose)
if not verbose and (validate or not modules or modules == "all"):
verbose = 1
exit_code = 0
count = 0
pythons = runez.flattened(pythons, split=",")
for spec in pythons:
if count:
print()
count += 1
if spec != "invoker":
spec = runez.resolved_path(spec)
inspector = PythonInspector(spec, modules)
if inspector.python.problem:
print("%s: %s" % (runez.blue(runez.short(inspector.python.executable)), runez.red(inspector.python.problem)))
exit_code = 1
continue
if verbose > 1 or len(pythons) > 1:
print(runez.blue(inspector.python))
print(inspector.represented(verbose=verbose))
if not inspector.full_so_report.is_valid:
exit_code = 1
if validate:
sys.exit(exit_code)
@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 = PythonVersions.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))
@main.command()
@click.option("--modules", "-m", metavar="CSV", help="External modules to include")
@click.option("--validate", is_flag=True, help="Exit with code 1 if anything looks incomplete")
@click.argument("python_spec", required=False)
def scan(modules, validate, python_spec):
"""Show status of buildable modules, which will be auto-compiled"""
setup = BuildSetup(python_spec, CONFIG_PATH, modules=modules)
print(runez.bold(setup.python_spec))
report = setup.python_builder.modules.report()
print(report)
if validate:
setup.validate_module_selection()
def _find_recompress_source(dist, path):
path = runez.to_path(path)
if path.exists() or path.is_absolute():
return path.absolute() if path.exists() else None
candidates = ["{path}", "build/{path}", "build/cpython-{path}/{path}"]
for candidate in candidates:
cp = candidate.format(path=path)
for p in (dist / cp, dist.parent / cp):
if p.exists():
return p.absolute()
@runez.log.timeit
def recompress_folder(dist, path, extension):
"""Recompress folder"""
dest = runez.SYS_INFO.platform_id.composed_basename("cpython", path.name, extension=extension)
dest = dist / dest
runez.compress(path, dest, logger=print)
return dest
@runez.log.timeit
def recompress_archive(dist, path, extension):
stem = path.name.rpartition(".")[0]
if stem.endswith(".tar"):
stem = stem.rpartition(".")[0]
dest = "%s.%s" % (stem, extension)
dest = dist / dest
if dest == path:
dest = "%s.%s" % (stem + "-recompressed", extension)
dest = 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
"""
extension = runez.SYS_INFO.platform_id.canonical_compress_extension(ext)
config = Config(CONFIG_PATH)
dist = config.dist_folder
with runez.Anchored(dist.parent):
actual_path = _find_recompress_source(dist, path)
if not actual_path:
runez.abort("'%s' does not exist" % runez.red(runez.short(path)))
if actual_path.is_dir():
dest = recompress_folder(dist, actual_path, extension)
else:
dest = recompress_archive(dist, 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))))
if __name__ == "__main__":
from portable_python.cli import main # noqa, re-import with proper package
main()