fromager 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fromager/__main__.py +489 -0
- fromager/context.py +108 -0
- fromager/dependencies.py +165 -0
- fromager/example_override.py +6 -0
- fromager/external_commands.py +46 -0
- fromager/extras_provider.py +54 -0
- fromager/finders.py +161 -0
- fromager/overrides.py +78 -0
- fromager/resolver.py +203 -0
- fromager/sdist.py +290 -0
- fromager/server.py +57 -0
- fromager/settings.py +29 -0
- fromager/sources.py +175 -0
- fromager/version.py +16 -0
- fromager/wheels.py +81 -0
- fromager-0.0.1.dist-info/LICENSE +202 -0
- fromager-0.0.1.dist-info/METADATA +108 -0
- fromager-0.0.1.dist-info/RECORD +21 -0
- fromager-0.0.1.dist-info/WHEEL +5 -0
- fromager-0.0.1.dist-info/entry_points.txt +5 -0
- fromager-0.0.1.dist-info/top_level.txt +1 -0
fromager/__main__.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import collections
|
|
5
|
+
import csv
|
|
6
|
+
import functools
|
|
7
|
+
import itertools
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from packaging.requirements import Requirement
|
|
15
|
+
|
|
16
|
+
from . import (context, finders, overrides, sdist, server, settings, sources,
|
|
17
|
+
wheels)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
TERSE_LOG_FMT = '%(message)s'
|
|
22
|
+
VERBOSE_LOG_FMT = '%(levelname)s:%(name)s:%(lineno)d: %(message)s'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
parser = _get_argument_parser()
|
|
27
|
+
args = parser.parse_args(sys.argv[1:])
|
|
28
|
+
|
|
29
|
+
# Configure console and log output.
|
|
30
|
+
stream_handler = logging.StreamHandler()
|
|
31
|
+
stream_handler.setLevel(logging.DEBUG if args.verbose else logging.INFO)
|
|
32
|
+
stream_formatter = logging.Formatter(VERBOSE_LOG_FMT if args.verbose else TERSE_LOG_FMT)
|
|
33
|
+
stream_handler.setFormatter(stream_formatter)
|
|
34
|
+
logging.getLogger().addHandler(stream_handler)
|
|
35
|
+
if args.log_file:
|
|
36
|
+
# Always log to the file at debug level
|
|
37
|
+
file_handler = logging.FileHandler(args.log_file)
|
|
38
|
+
file_handler.setLevel(logging.DEBUG)
|
|
39
|
+
file_formatter = logging.Formatter(VERBOSE_LOG_FMT)
|
|
40
|
+
file_handler.setFormatter(file_formatter)
|
|
41
|
+
logging.getLogger().addHandler(file_handler)
|
|
42
|
+
# We need to set the overall logger level to debug and allow the
|
|
43
|
+
# handlers to filter messages at their own level.
|
|
44
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
args.func(args)
|
|
48
|
+
except Exception as err:
|
|
49
|
+
logger.exception(err)
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_argument_parser():
|
|
54
|
+
parser = argparse.ArgumentParser('fromager')
|
|
55
|
+
parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
|
56
|
+
help='report more detail to the console')
|
|
57
|
+
parser.add_argument('--log-file', default='',
|
|
58
|
+
help='save detailed report of actions to file')
|
|
59
|
+
parser.add_argument('-o', '--sdists-repo', default='sdists-repo',
|
|
60
|
+
help='location to manage source distributions [%(default)s]')
|
|
61
|
+
parser.add_argument('-w', '--wheels-repo', default='wheels-repo',
|
|
62
|
+
help='location to manage wheel repository [%(default)s]')
|
|
63
|
+
parser.add_argument('-t', '--work-dir', default=os.environ.get('WORKDIR', 'work-dir'),
|
|
64
|
+
help='location to manage working files, including builds [%(default)s]')
|
|
65
|
+
parser.add_argument('-p', '--patches-dir', default='overrides/patches',
|
|
66
|
+
help='location of files for patching source before building [%(default)s]')
|
|
67
|
+
parser.add_argument('-e', '--envs-dir', default='overrides/envs',
|
|
68
|
+
help='location of environment override files [%(default)s]')
|
|
69
|
+
parser.add_argument('--settings-file', default='overrides/settings.yaml',
|
|
70
|
+
help='location of the application settings file [%(default)s]')
|
|
71
|
+
parser.add_argument('--wheel-server-url',
|
|
72
|
+
help='URL for the wheel server for builds')
|
|
73
|
+
parser.add_argument('--no-cleanup', dest='cleanup', default=True, action='store_false',
|
|
74
|
+
help='do not remove working files when a build completes successfully')
|
|
75
|
+
parser.add_argument('--variant', default='cpu',
|
|
76
|
+
help='the build variant name [%(default)s]')
|
|
77
|
+
|
|
78
|
+
subparsers = parser.add_subparsers(title='commands', dest='command')
|
|
79
|
+
|
|
80
|
+
parser_bootstrap = subparsers.add_parser(
|
|
81
|
+
'bootstrap',
|
|
82
|
+
help='recursively build packages and their dependencies',
|
|
83
|
+
)
|
|
84
|
+
parser_bootstrap.set_defaults(func=do_bootstrap)
|
|
85
|
+
parser_bootstrap.add_argument('--requirements-file', '-r', action='append', default=[],
|
|
86
|
+
dest='requirements_files',
|
|
87
|
+
help='a pip requirements file')
|
|
88
|
+
parser_bootstrap.add_argument('toplevel', nargs='*',
|
|
89
|
+
help='a requirements specification for a package')
|
|
90
|
+
|
|
91
|
+
parser_download = subparsers.add_parser(
|
|
92
|
+
'download-source-archive',
|
|
93
|
+
help='download the source code archive for one version of one package',
|
|
94
|
+
)
|
|
95
|
+
parser_download.set_defaults(func=do_download_source_archive)
|
|
96
|
+
parser_download.add_argument('dist_name',
|
|
97
|
+
help='the name of the distribution')
|
|
98
|
+
parser_download.add_argument('dist_version',
|
|
99
|
+
help='the version of the distribution')
|
|
100
|
+
parser_download.add_argument('sdist_server_url',
|
|
101
|
+
help='the URL for a PyPI-compatible package index hosting sdists')
|
|
102
|
+
|
|
103
|
+
parser_prepare_source = subparsers.add_parser(
|
|
104
|
+
'prepare-source',
|
|
105
|
+
help='ensure the source code is in a form ready for building a distribution',
|
|
106
|
+
)
|
|
107
|
+
parser_prepare_source.set_defaults(func=do_prepare_source)
|
|
108
|
+
parser_prepare_source.add_argument('dist_name',
|
|
109
|
+
help='the name of the distribution')
|
|
110
|
+
parser_prepare_source.add_argument('dist_version',
|
|
111
|
+
help='the version of the distribution')
|
|
112
|
+
|
|
113
|
+
parser_prepare_build = subparsers.add_parser(
|
|
114
|
+
'prepare-build',
|
|
115
|
+
help='set up build environment to build the package',
|
|
116
|
+
)
|
|
117
|
+
parser_prepare_build.set_defaults(func=do_prepare_build)
|
|
118
|
+
parser_prepare_build.add_argument('dist_name',
|
|
119
|
+
help='the name of the distribution')
|
|
120
|
+
parser_prepare_build.add_argument('dist_version',
|
|
121
|
+
help='the version of the distribution')
|
|
122
|
+
|
|
123
|
+
parser_build = subparsers.add_parser(
|
|
124
|
+
'build',
|
|
125
|
+
help='build a wheel',
|
|
126
|
+
)
|
|
127
|
+
parser_build.set_defaults(func=do_build)
|
|
128
|
+
parser_build.add_argument('dist_name',
|
|
129
|
+
help='the name of the distribution')
|
|
130
|
+
parser_build.add_argument('dist_version',
|
|
131
|
+
help='the version of the distribution')
|
|
132
|
+
|
|
133
|
+
parser_canonicalize = subparsers.add_parser(
|
|
134
|
+
'canonicalize',
|
|
135
|
+
help='convert a package name to its canonical form for use in override paths',
|
|
136
|
+
)
|
|
137
|
+
parser_canonicalize.set_defaults(func=do_canonicalize)
|
|
138
|
+
parser_canonicalize.add_argument('toplevel', nargs='+',
|
|
139
|
+
help='names of distributions to convert')
|
|
140
|
+
|
|
141
|
+
parser_csv = subparsers.add_parser(
|
|
142
|
+
'build-order-csv',
|
|
143
|
+
help='convert build order files to CSV',
|
|
144
|
+
)
|
|
145
|
+
parser_csv.set_defaults(func=do_build_order_csv)
|
|
146
|
+
parser_csv.add_argument('build_order_file', default='work-dir/build-order.json', nargs='?',
|
|
147
|
+
help='the build-order.json files to convert')
|
|
148
|
+
parser_csv.add_argument('--output', '-o',
|
|
149
|
+
help='write the output to a named file (defaults to console)')
|
|
150
|
+
|
|
151
|
+
parser_graph = subparsers.add_parser(
|
|
152
|
+
'build-order-graph',
|
|
153
|
+
help='convert build-order.json files to a dot graph showing dependencies',
|
|
154
|
+
)
|
|
155
|
+
parser_graph.set_defaults(func=do_build_order_graph)
|
|
156
|
+
parser_graph.add_argument('build_order_file', nargs='+',
|
|
157
|
+
help='the build-order.json files to convert')
|
|
158
|
+
parser_graph.add_argument('--output', '-o',
|
|
159
|
+
help='write the output to a named file (defaults to console)')
|
|
160
|
+
|
|
161
|
+
parser_summary = subparsers.add_parser(
|
|
162
|
+
'build-order-summary',
|
|
163
|
+
help='report commonalities and differences between build order files',
|
|
164
|
+
)
|
|
165
|
+
parser_summary.set_defaults(func=do_build_order_summary)
|
|
166
|
+
parser_summary.add_argument('build_order_file', nargs='+',
|
|
167
|
+
help='the build-order.json files to examine')
|
|
168
|
+
parser_summary.add_argument('--output', '-o',
|
|
169
|
+
help='write the output to a named CSV file (defaults to console)')
|
|
170
|
+
|
|
171
|
+
return parser
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def requires_context(f):
|
|
175
|
+
"Decorate f() to add WorkContext argument before calling it."
|
|
176
|
+
@functools.wraps(f)
|
|
177
|
+
def provides_context(args):
|
|
178
|
+
ctx = context.WorkContext(
|
|
179
|
+
settings=settings.load(args.settings_file),
|
|
180
|
+
patches_dir=args.patches_dir,
|
|
181
|
+
envs_dir=args.envs_dir,
|
|
182
|
+
sdists_repo=args.sdists_repo,
|
|
183
|
+
wheels_repo=args.wheels_repo,
|
|
184
|
+
work_dir=args.work_dir,
|
|
185
|
+
wheel_server_url=args.wheel_server_url,
|
|
186
|
+
cleanup=args.cleanup,
|
|
187
|
+
variant=args.variant,
|
|
188
|
+
)
|
|
189
|
+
ctx.setup()
|
|
190
|
+
return f(args, ctx)
|
|
191
|
+
return provides_context
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _get_requirements_from_args(args):
|
|
195
|
+
to_build = []
|
|
196
|
+
to_build.extend(args.toplevel)
|
|
197
|
+
for filename in args.requirements_files:
|
|
198
|
+
with open(filename, 'r') as f:
|
|
199
|
+
for line in f:
|
|
200
|
+
useful, _, _ = line.partition('#')
|
|
201
|
+
useful = useful.strip()
|
|
202
|
+
logger.debug('line %r useful %r', line, useful)
|
|
203
|
+
if not useful:
|
|
204
|
+
continue
|
|
205
|
+
to_build.append(useful)
|
|
206
|
+
return to_build
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@requires_context
|
|
210
|
+
def do_bootstrap(args, ctx):
|
|
211
|
+
server.start_wheel_server(ctx)
|
|
212
|
+
|
|
213
|
+
to_build = _get_requirements_from_args(args)
|
|
214
|
+
if not to_build:
|
|
215
|
+
raise RuntimeError('Pass a requirement specificiation or use -r to pass a requirements file')
|
|
216
|
+
logger.debug('bootstrapping %s', to_build)
|
|
217
|
+
for toplevel in to_build:
|
|
218
|
+
sdist.handle_requirement(ctx, Requirement(toplevel))
|
|
219
|
+
|
|
220
|
+
# If we put pre-built wheels in the downloads directory, we should
|
|
221
|
+
# remove them so we can treat that directory as a source of wheels
|
|
222
|
+
# to upload to an index.
|
|
223
|
+
for prebuilt_wheel in ctx.wheels_prebuilt.glob('*.whl'):
|
|
224
|
+
filename = ctx.wheels_downloads / prebuilt_wheel.name
|
|
225
|
+
if filename.exists():
|
|
226
|
+
logger.info(f'removing prebuilt wheel {prebuilt_wheel.name} from download cache')
|
|
227
|
+
filename.unlink()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@requires_context
|
|
231
|
+
def do_download_source_archive(args, ctx):
|
|
232
|
+
req = Requirement(f'{args.dist_name}=={args.dist_version}')
|
|
233
|
+
logger.info('downloading source archive for %s from %s', req, args.sdist_server_url)
|
|
234
|
+
filename, _ = sources.download_source(ctx, req, [args.sdist_server_url])
|
|
235
|
+
print(filename)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@requires_context
|
|
239
|
+
def do_prepare_source(args, ctx):
|
|
240
|
+
req = Requirement(f'{args.dist_name}=={args.dist_version}')
|
|
241
|
+
logger.info('preparing source directory for %s', req)
|
|
242
|
+
sdists_downloads = pathlib.Path(args.sdists_repo) / 'downloads'
|
|
243
|
+
source_filename = finders.find_sdist(sdists_downloads, req, args.dist_version)
|
|
244
|
+
if source_filename is None:
|
|
245
|
+
dir_contents = []
|
|
246
|
+
for ext in ['*.tar.gz', '*.zip']:
|
|
247
|
+
dir_contents.extend(str(e) for e in sdists_downloads.glob(ext))
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
f'Cannot find sdist for {req.name} version {args.dist_version} in {sdists_downloads} among {dir_contents}'
|
|
250
|
+
)
|
|
251
|
+
# FIXME: Does the version need to be a Version instead of str?
|
|
252
|
+
source_root_dir = sources.prepare_source(ctx, req, source_filename, args.dist_version)
|
|
253
|
+
print(source_root_dir)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _find_source_root_dir(work_dir, req, dist_version):
|
|
257
|
+
source_root_dir = finders.find_source_dir(pathlib.Path(work_dir), req, dist_version)
|
|
258
|
+
if source_root_dir:
|
|
259
|
+
return source_root_dir
|
|
260
|
+
work_dir_contents = list(str(e) for e in work_dir.glob('*'))
|
|
261
|
+
raise RuntimeError(
|
|
262
|
+
f'Cannot find source directory for {req.name} version {dist_version} among {work_dir_contents}'
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@requires_context
|
|
267
|
+
def do_prepare_build(args, ctx):
|
|
268
|
+
server.start_wheel_server(ctx)
|
|
269
|
+
req = Requirement(f'{args.dist_name}=={args.dist_version}')
|
|
270
|
+
source_root_dir = _find_source_root_dir(pathlib.Path(args.work_dir), req, args.dist_version)
|
|
271
|
+
logger.info('preparing build environment for %s', req)
|
|
272
|
+
sdist.prepare_build_environment(ctx, req, source_root_dir)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@requires_context
|
|
276
|
+
def do_build(args, ctx):
|
|
277
|
+
req = Requirement(f'{args.dist_name}=={args.dist_version}')
|
|
278
|
+
logger.info('building for %s', req)
|
|
279
|
+
source_root_dir = _find_source_root_dir(pathlib.Path(args.work_dir), req, args.dist_version)
|
|
280
|
+
build_env = wheels.BuildEnvironment(ctx, source_root_dir.parent, None)
|
|
281
|
+
wheel_filename = wheels.build_wheel(ctx, req, source_root_dir, build_env)
|
|
282
|
+
print(wheel_filename)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def do_canonicalize(args):
|
|
286
|
+
for name in args.toplevel:
|
|
287
|
+
print(overrides.pkgname_to_override_module(name))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def do_build_order_csv(args):
|
|
291
|
+
fields = [
|
|
292
|
+
('dist', 'Distribution Name'),
|
|
293
|
+
('version', 'Version'),
|
|
294
|
+
('req', 'Original Requirement'),
|
|
295
|
+
('type', 'Dependency Type'),
|
|
296
|
+
('prebuilt', 'Pre-built Package'),
|
|
297
|
+
('order', 'Build Order'),
|
|
298
|
+
('why', 'Dependency Chain'),
|
|
299
|
+
]
|
|
300
|
+
headers = {n: v for n, v in fields}
|
|
301
|
+
fieldkeys = [f[0] for f in fields]
|
|
302
|
+
fieldnames = [f[1] for f in fields]
|
|
303
|
+
|
|
304
|
+
build_order = []
|
|
305
|
+
with open(args.build_order_file, 'r') as f:
|
|
306
|
+
for i, entry in enumerate(json.load(f), 1):
|
|
307
|
+
# Add an order column, not in the original source file, in
|
|
308
|
+
# case someone wants to sort the output on another field.
|
|
309
|
+
entry['order'] = i
|
|
310
|
+
# Replace the short keys with the longer human-readable
|
|
311
|
+
# headers we want in the CSV output.
|
|
312
|
+
new_entry = {headers[f]: entry[f] for f in fieldkeys}
|
|
313
|
+
# Reformat the why field
|
|
314
|
+
new_entry['Dependency Chain'] = ' '.join(
|
|
315
|
+
f'-{dep_type}-> {Requirement(req).name}({version})'
|
|
316
|
+
for dep_type, req, version
|
|
317
|
+
in entry['why']
|
|
318
|
+
)
|
|
319
|
+
build_order.append(new_entry)
|
|
320
|
+
|
|
321
|
+
if args.output:
|
|
322
|
+
outfile = open(args.output, 'w')
|
|
323
|
+
else:
|
|
324
|
+
outfile = sys.stdout
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
writer = csv.DictWriter(outfile, fieldnames=fieldnames, quoting=csv.QUOTE_NONNUMERIC)
|
|
328
|
+
writer.writeheader()
|
|
329
|
+
writer.writerows(build_order)
|
|
330
|
+
finally:
|
|
331
|
+
if args.output:
|
|
332
|
+
outfile.close()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def do_build_order_summary(args):
|
|
336
|
+
dist_to_input_file = collections.defaultdict(dict)
|
|
337
|
+
for filename in args.build_order_file:
|
|
338
|
+
with open(filename, 'r') as f:
|
|
339
|
+
build_order = json.load(f)
|
|
340
|
+
for step in build_order:
|
|
341
|
+
key = overrides.pkgname_to_override_module(step['dist'])
|
|
342
|
+
dist_to_input_file[key][filename] = step['version']
|
|
343
|
+
|
|
344
|
+
if args.output:
|
|
345
|
+
outfile = open(args.output, 'w')
|
|
346
|
+
else:
|
|
347
|
+
outfile = sys.stdout
|
|
348
|
+
|
|
349
|
+
# The build order files are organized in directories named for the
|
|
350
|
+
# image. Pull those names out of the files given.
|
|
351
|
+
image_column_names = tuple(
|
|
352
|
+
pathlib.Path(filename).parent.name
|
|
353
|
+
for filename in args.build_order_file
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
writer = csv.writer(outfile, quoting=csv.QUOTE_NONNUMERIC)
|
|
357
|
+
writer.writerow(("Distribution Name",) + image_column_names + ("Same Version",))
|
|
358
|
+
for dist, present_in_files in sorted(dist_to_input_file.items()):
|
|
359
|
+
all_versions = set()
|
|
360
|
+
row = [dist]
|
|
361
|
+
for filename in args.build_order_file:
|
|
362
|
+
v = present_in_files.get(filename, "")
|
|
363
|
+
row.append(v)
|
|
364
|
+
if v:
|
|
365
|
+
all_versions.add(v)
|
|
366
|
+
row.append(len(all_versions) == 1)
|
|
367
|
+
writer.writerow(row)
|
|
368
|
+
|
|
369
|
+
if args.output:
|
|
370
|
+
outfile.close()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def do_build_order_graph(args):
|
|
374
|
+
|
|
375
|
+
def fmt_req(req, version):
|
|
376
|
+
req = Requirement(req)
|
|
377
|
+
name = overrides.pkgname_to_override_module(req.name)
|
|
378
|
+
return f'{name}{"[" + ",".join(req.extras) + "]" if req.extras else ""}=={version}'
|
|
379
|
+
|
|
380
|
+
def new_node(req):
|
|
381
|
+
if req not in nodes:
|
|
382
|
+
nodes[req] = {
|
|
383
|
+
'nid': 'node' + str(next(node_ids)),
|
|
384
|
+
'prebuilt': False,
|
|
385
|
+
}
|
|
386
|
+
return nodes[req]
|
|
387
|
+
|
|
388
|
+
def update_node(req, prebuilt=False):
|
|
389
|
+
node_details = new_node(req)
|
|
390
|
+
if (not node_details['prebuilt']) and prebuilt:
|
|
391
|
+
node_details['prebuilt'] = True
|
|
392
|
+
return req
|
|
393
|
+
|
|
394
|
+
# Track unique ids for nodes since the labels may not be
|
|
395
|
+
# syntactically correct.
|
|
396
|
+
node_ids = itertools.count(1)
|
|
397
|
+
# Map formatted requirement text to node details
|
|
398
|
+
nodes = {}
|
|
399
|
+
edges = []
|
|
400
|
+
|
|
401
|
+
for filename in args.build_order_file:
|
|
402
|
+
with open(filename, 'r') as f:
|
|
403
|
+
build_order = json.load(f)
|
|
404
|
+
|
|
405
|
+
for step in build_order:
|
|
406
|
+
update_node(fmt_req(step['dist'], step['version']), prebuilt=step['prebuilt'])
|
|
407
|
+
try:
|
|
408
|
+
why = step['why']
|
|
409
|
+
if len(why) == 0:
|
|
410
|
+
# should not happen
|
|
411
|
+
continue
|
|
412
|
+
elif len(why) == 1:
|
|
413
|
+
# Lone node requiring nothing to build.
|
|
414
|
+
pass
|
|
415
|
+
else:
|
|
416
|
+
parent_info = why[0]
|
|
417
|
+
for child_info in why[1:]:
|
|
418
|
+
parent = update_node(fmt_req(parent_info[1], parent_info[2]))
|
|
419
|
+
child = update_node(fmt_req(child_info[1], child_info[2]))
|
|
420
|
+
edge = (parent, child)
|
|
421
|
+
# print(edge, nodes[edge[0]], nodes[edge[1]])
|
|
422
|
+
if edge not in edges:
|
|
423
|
+
edges.append(edge)
|
|
424
|
+
parent_info = child_info
|
|
425
|
+
except Exception as err:
|
|
426
|
+
raise Exception(f'Error processing {filename} at {step}') from err
|
|
427
|
+
|
|
428
|
+
if args.output:
|
|
429
|
+
outfile = open(args.output, 'w')
|
|
430
|
+
else:
|
|
431
|
+
outfile = sys.stdout
|
|
432
|
+
try:
|
|
433
|
+
|
|
434
|
+
outfile.write('digraph {\n')
|
|
435
|
+
|
|
436
|
+
# Determine some nodes with special characteristics
|
|
437
|
+
all_nodes = set(n['nid'] for n in nodes.values())
|
|
438
|
+
# left = set(nodes[p]['nid'] for p, _ in edges)
|
|
439
|
+
right = set(nodes[c]['nid'] for _, c in edges)
|
|
440
|
+
# Toplevel nodes have no incoming connections
|
|
441
|
+
toplevel_nodes = all_nodes - right
|
|
442
|
+
# Leaves have no outgoing connections
|
|
443
|
+
# leaves = all_nodes - left
|
|
444
|
+
|
|
445
|
+
for req, node_details in nodes.items():
|
|
446
|
+
nid = node_details['nid']
|
|
447
|
+
|
|
448
|
+
node_attrs = [('label', req)]
|
|
449
|
+
if node_details['prebuilt']:
|
|
450
|
+
node_attrs.extend([
|
|
451
|
+
('style', 'filled'),
|
|
452
|
+
('color', 'darkred'),
|
|
453
|
+
('fontcolor', 'white'),
|
|
454
|
+
('tooltip', 'pre-built package'),
|
|
455
|
+
])
|
|
456
|
+
elif nid in toplevel_nodes:
|
|
457
|
+
node_attrs.extend([
|
|
458
|
+
('style', 'filled'),
|
|
459
|
+
('color', 'darkgreen'),
|
|
460
|
+
('fontcolor', 'white'),
|
|
461
|
+
('tooltip', 'toplevel package'),
|
|
462
|
+
])
|
|
463
|
+
node_attr_text = ','.join('%s="%s"' % a for a in node_attrs)
|
|
464
|
+
|
|
465
|
+
outfile.write(f' {nid} [{node_attr_text}];\n')
|
|
466
|
+
|
|
467
|
+
outfile.write('\n')
|
|
468
|
+
if len(toplevel_nodes) > 1:
|
|
469
|
+
outfile.write(' /* toplevel nodes should all be at the same level */\n')
|
|
470
|
+
outfile.write(' {rank=same; %s;}\n\n' % " ".join(toplevel_nodes))
|
|
471
|
+
# if len(leaves) > 1:
|
|
472
|
+
# outfile.write(' /* leaf nodes should all be at the same level */\n')
|
|
473
|
+
# outfile.write(' {rank=same; %s;}\n\n' % " ".join(leaves))
|
|
474
|
+
|
|
475
|
+
for parent_req, child_req in edges:
|
|
476
|
+
parent_node = nodes[parent_req]
|
|
477
|
+
parent_nid = parent_node['nid']
|
|
478
|
+
child_node = nodes[child_req]
|
|
479
|
+
child_nid = child_node['nid']
|
|
480
|
+
outfile.write(f' {parent_nid} -> {child_nid};\n')
|
|
481
|
+
|
|
482
|
+
outfile.write('}\n')
|
|
483
|
+
finally:
|
|
484
|
+
if args.output:
|
|
485
|
+
outfile.close()
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
if __name__ == '__main__':
|
|
489
|
+
main()
|
fromager/context.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import pathlib
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
from packaging.utils import canonicalize_name
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkContext:
|
|
12
|
+
|
|
13
|
+
def __init__(self,
|
|
14
|
+
settings,
|
|
15
|
+
patches_dir,
|
|
16
|
+
envs_dir,
|
|
17
|
+
sdists_repo,
|
|
18
|
+
wheels_repo,
|
|
19
|
+
work_dir,
|
|
20
|
+
wheel_server_url,
|
|
21
|
+
cleanup=True,
|
|
22
|
+
variant='cpu'):
|
|
23
|
+
self.settings = settings
|
|
24
|
+
self.patches_dir = pathlib.Path(patches_dir).absolute()
|
|
25
|
+
self.envs_dir = pathlib.Path(envs_dir).absolute()
|
|
26
|
+
self.sdists_repo = pathlib.Path(sdists_repo).absolute()
|
|
27
|
+
self.sdists_downloads = self.sdists_repo / 'downloads'
|
|
28
|
+
self.wheels_repo = pathlib.Path(wheels_repo).absolute()
|
|
29
|
+
self.wheels_build = self.wheels_repo / 'build'
|
|
30
|
+
self.wheels_downloads = self.wheels_repo / 'downloads'
|
|
31
|
+
self.wheels_prebuilt = self.wheels_repo / 'prebuilt'
|
|
32
|
+
self.wheel_server_dir = self.wheels_repo / 'simple'
|
|
33
|
+
self.work_dir = pathlib.Path(work_dir).absolute()
|
|
34
|
+
self.wheel_server_url = wheel_server_url
|
|
35
|
+
self.cleanup = cleanup
|
|
36
|
+
self.variant = variant
|
|
37
|
+
|
|
38
|
+
self._build_order_filename = self.work_dir / 'build-order.json'
|
|
39
|
+
|
|
40
|
+
# Push items onto the stack as we start to resolve their
|
|
41
|
+
# dependencies so at the end we have a list of items that need to
|
|
42
|
+
# be built in order.
|
|
43
|
+
self._build_stack = []
|
|
44
|
+
self._build_requirements = set()
|
|
45
|
+
|
|
46
|
+
# Track requirements we've seen before so we don't resolve the
|
|
47
|
+
# same dependencies over and over and so we can break cycles in
|
|
48
|
+
# the dependency list. The key is the requirements spec, rather
|
|
49
|
+
# than the package, in case we do have multiple rules for the same
|
|
50
|
+
# package.
|
|
51
|
+
self._seen_requirements = set()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def pip_wheel_server_args(self):
|
|
55
|
+
args = ['--index-url', self.wheel_server_url]
|
|
56
|
+
parsed = urlparse(self.wheel_server_url)
|
|
57
|
+
if parsed.scheme != 'https':
|
|
58
|
+
args = args + [
|
|
59
|
+
'--trusted-host', parsed.hostname
|
|
60
|
+
]
|
|
61
|
+
return args
|
|
62
|
+
|
|
63
|
+
def _resolved_key(self, req, version):
|
|
64
|
+
return (canonicalize_name(req.name), tuple(sorted(req.extras)), str(version))
|
|
65
|
+
|
|
66
|
+
def mark_as_seen(self, req, version):
|
|
67
|
+
logger.debug('remembering seen sdist %s', self._resolved_key(req, version))
|
|
68
|
+
self._seen_requirements.add(self._resolved_key(req, version))
|
|
69
|
+
|
|
70
|
+
def has_been_seen(self, req, version):
|
|
71
|
+
return self._resolved_key(req, version) in self._seen_requirements
|
|
72
|
+
|
|
73
|
+
def add_to_build_order(self, req_type, req, version, why, prebuilt=False):
|
|
74
|
+
# We only care if this version of this package has been built,
|
|
75
|
+
# and don't want to trigger building it twice. The "extras"
|
|
76
|
+
# value, included in the _resolved_key() output, can confuse
|
|
77
|
+
# that so we ignore itand build our own key using just the
|
|
78
|
+
# name and version.
|
|
79
|
+
key = (canonicalize_name(req.name), str(version))
|
|
80
|
+
if key in self._build_requirements:
|
|
81
|
+
return
|
|
82
|
+
logger.info(f'adding {key} to build order')
|
|
83
|
+
self._build_requirements.add(key)
|
|
84
|
+
info = {
|
|
85
|
+
'type': req_type,
|
|
86
|
+
'req': str(req),
|
|
87
|
+
'dist': canonicalize_name(req.name),
|
|
88
|
+
'version': str(version),
|
|
89
|
+
'why': why,
|
|
90
|
+
'prebuilt': prebuilt,
|
|
91
|
+
}
|
|
92
|
+
self._build_stack.append(info)
|
|
93
|
+
with open(self._build_order_filename, 'w') as f:
|
|
94
|
+
# Set default=str because the why value includes
|
|
95
|
+
# Requirement and Version instances that can't be
|
|
96
|
+
# converted to JSON without help.
|
|
97
|
+
json.dump(self._build_stack, f, indent=2, default=str)
|
|
98
|
+
|
|
99
|
+
def setup(self):
|
|
100
|
+
# The work dir must already exist, so don't try to create it.
|
|
101
|
+
# Use os.makedirs() to create the others in case the paths
|
|
102
|
+
# already exist.
|
|
103
|
+
for p in [self.work_dir,
|
|
104
|
+
self.sdists_repo, self.sdists_downloads,
|
|
105
|
+
self.wheels_repo, self.wheels_downloads, self.wheels_prebuilt]:
|
|
106
|
+
if not p.exists():
|
|
107
|
+
logger.debug('creating %s', p)
|
|
108
|
+
p.mkdir(parents=True)
|