flatpak-cargo-generator 0.1.0__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.
File without changes
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+
3
+ __license__ = 'MIT'
4
+ import json
5
+ from urllib.parse import urlparse, ParseResult, parse_qs
6
+ import os
7
+ import contextlib
8
+ import copy
9
+ import glob
10
+ import subprocess
11
+ import argparse
12
+ import logging
13
+ import hashlib
14
+ import asyncio
15
+ from typing import Any, Dict, List, NamedTuple, Optional, Tuple, TypedDict
16
+
17
+ import aiohttp
18
+ import toml
19
+
20
+ CRATES_IO = 'https://static.crates.io/crates'
21
+ CARGO_HOME = 'cargo'
22
+ CARGO_CRATES = f'{CARGO_HOME}/vendor'
23
+ VENDORED_SOURCES = 'vendored-sources'
24
+ GIT_CACHE = 'flatpak-cargo/git'
25
+ COMMIT_LEN = 7
26
+
27
+
28
+ @contextlib.contextmanager
29
+ def workdir(path: str):
30
+ oldpath = os.getcwd()
31
+ os.chdir(path)
32
+ try:
33
+ yield
34
+ finally:
35
+ os.chdir(oldpath)
36
+
37
+
38
+ def canonical_url(url: str) -> ParseResult:
39
+ 'Converts a string to a Cargo Canonical URL, as per https://github.com/rust-lang/cargo/blob/35c55a93200c84a4de4627f1770f76a8ad268a39/src/cargo/util/canonical_url.rs#L19'
40
+ # Hrm. The upstream cargo does not replace those URLs, but if we don't then it doesn't work too well :(
41
+ url = url.replace('git+https://', 'https://')
42
+ u = urlparse(url)
43
+ # It seems cargo drops query and fragment
44
+ u = ParseResult(u.scheme, u.netloc, u.path, '', '', '')
45
+ u = u._replace(path = u.path.rstrip('/'))
46
+
47
+ if u.netloc == 'github.com':
48
+ u = u._replace(scheme = 'https')
49
+ u = u._replace(path = u.path.lower())
50
+
51
+ if u.path.endswith('.git'):
52
+ u = u._replace(path = u.path[:-len('.git')])
53
+
54
+ return u
55
+
56
+
57
+ def get_git_tarball(repo_url: str, commit: str) -> str:
58
+ url = canonical_url(repo_url)
59
+ path = url.path.split('/')[1:]
60
+
61
+ assert len(path) == 2
62
+ owner = path[0]
63
+ if path[1].endswith('.git'):
64
+ repo = path[1].replace('.git', '')
65
+ else:
66
+ repo = path[1]
67
+ if url.hostname == 'github.com':
68
+ return f'https://codeload.{url.hostname}/{owner}/{repo}/tar.gz/{commit}'
69
+ elif url.hostname.split('.')[0] == 'gitlab': # type: ignore
70
+ return f'https://{url.hostname}/{owner}/{repo}/-/archive/{commit}/{repo}-{commit}.tar.gz'
71
+ elif url.hostname == 'bitbucket.org':
72
+ return f'https://{url.hostname}/{owner}/{repo}/get/{commit}.tar.gz'
73
+ else:
74
+ raise ValueError(f'Don\'t know how to get tarball for {repo_url}')
75
+
76
+
77
+ async def get_remote_sha256(url: str) -> str:
78
+ logging.info(f"started sha256({url})")
79
+ sha256 = hashlib.sha256()
80
+ async with aiohttp.ClientSession(raise_for_status=True) as http_session:
81
+ async with http_session.get(url) as response:
82
+ while True:
83
+ data = await response.content.read(4096)
84
+ if not data:
85
+ break
86
+ sha256.update(data)
87
+ logging.info(f"done sha256({url})")
88
+ return sha256.hexdigest()
89
+
90
+
91
+ _TomlType = Dict[str, Any]
92
+
93
+
94
+ def load_toml(tomlfile: str = 'Cargo.lock') -> _TomlType:
95
+ with open(tomlfile, 'r') as f:
96
+ toml_data = toml.load(f)
97
+ return toml_data
98
+
99
+
100
+ def git_repo_name(git_url: str, commit: str) -> str:
101
+ name = canonical_url(git_url).path.split('/')[-1]
102
+ return f'{name}-{commit[:COMMIT_LEN]}'
103
+
104
+
105
+ def fetch_git_repo(git_url: str, commit: str) -> str:
106
+ repo_dir = git_url.replace('://', '_').replace('/', '_')
107
+ cache_dir = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
108
+ clone_dir = os.path.join(cache_dir, 'flatpak-cargo', repo_dir)
109
+ if not os.path.isdir(os.path.join(clone_dir, '.git')):
110
+ subprocess.run(['git', 'clone', '--depth=1', git_url, clone_dir], check=True)
111
+ rev_parse_proc = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=clone_dir, check=True,
112
+ stdout=subprocess.PIPE)
113
+ head = rev_parse_proc.stdout.decode().strip()
114
+ if head[:COMMIT_LEN] != commit[:COMMIT_LEN]:
115
+ subprocess.run(['git', 'fetch', 'origin', commit], cwd=clone_dir, check=True)
116
+ subprocess.run(['git', 'checkout', commit], cwd=clone_dir, check=True)
117
+
118
+ # Get the submodules as they might contain dependencies. This is a noop if
119
+ # there are no submodules in the repository
120
+ subprocess.run(['git', 'submodule', 'update', '--init', '--recursive'], cwd=clone_dir, check=True)
121
+
122
+ return clone_dir
123
+
124
+ def update_workspace_keys(pkg, workspace):
125
+ for key, item in pkg.items():
126
+ # There cannot be a 'workspace' key if the item is not a dict.
127
+ if not isinstance(item, dict):
128
+ continue;
129
+
130
+ # Recurse for keys under target.cfg(..)
131
+ if key == 'target':
132
+ for target in item.values():
133
+ update_workspace_keys(target, workspace)
134
+ continue;
135
+ # dev-dependencies and build-dependencies should reference root dependencies table from workspace
136
+ elif key == 'dev-dependencies' or key == 'build-dependencies':
137
+ update_workspace_keys(item, workspace.get('dependencies', None))
138
+ continue;
139
+
140
+ if not workspace or not key in workspace:
141
+ continue;
142
+
143
+ workspace_item = workspace[key]
144
+
145
+ if 'workspace' in item:
146
+ if isinstance(workspace_item, dict):
147
+ del item['workspace']
148
+
149
+ for dep_key, workspace_value in workspace_item.items():
150
+ # features are additive
151
+ if dep_key == 'features' and 'features' in item:
152
+ item['features'] += workspace_value
153
+ else:
154
+ item[dep_key] = workspace_value
155
+ elif len(item) > 1:
156
+ del item['workspace']
157
+ item.update({ 'version': workspace_item })
158
+ else:
159
+ pkg[key] = workspace_item
160
+ else:
161
+ update_workspace_keys(item, workspace_item)
162
+
163
+ class _GitPackage(NamedTuple):
164
+ path: str
165
+ package: _TomlType
166
+ workspace: Optional[_TomlType]
167
+
168
+ @property
169
+ def normalized(self) -> _TomlType:
170
+ package = copy.deepcopy(self.package)
171
+ if self.workspace is None:
172
+ return package
173
+
174
+ update_workspace_keys(package, self.workspace)
175
+
176
+ return package
177
+
178
+ _GitPackagesType = Dict[str, _GitPackage]
179
+
180
+
181
+ async def get_git_repo_packages(git_url: str, commit: str) -> _GitPackagesType:
182
+ logging.info('Loading packages from %s', git_url)
183
+ git_repo_dir = fetch_git_repo(git_url, commit)
184
+ packages: _GitPackagesType = {}
185
+
186
+ def get_cargo_toml_packages(root_dir: str, workspace: Optional[_TomlType] = None):
187
+ assert not os.path.isabs(root_dir) and os.path.isdir(root_dir)
188
+
189
+ with workdir(root_dir):
190
+ if os.path.exists('Cargo.toml'):
191
+ cargo_toml = load_toml('Cargo.toml')
192
+ workspace = cargo_toml.get('workspace') or workspace
193
+
194
+ if 'package' in cargo_toml:
195
+ packages[cargo_toml['package']['name']] = _GitPackage(
196
+ path=os.path.normpath(root_dir),
197
+ package=cargo_toml,
198
+ workspace=workspace
199
+ )
200
+ for child in os.scandir(root_dir):
201
+ if child.is_dir():
202
+ # the workspace can be referenced by any subdirectory
203
+ get_cargo_toml_packages(child.path, workspace)
204
+
205
+ with workdir(git_repo_dir):
206
+ get_cargo_toml_packages('.')
207
+
208
+ assert packages, f"No packages found in {git_repo_dir}"
209
+ logging.debug(
210
+ 'Packages in %s:\n%s',
211
+ git_url,
212
+ json.dumps(
213
+ {k: v.path for k, v in packages.items()},
214
+ indent=4,
215
+ ),
216
+ )
217
+ return packages
218
+
219
+
220
+ _FlatpakSourceType = Dict[str, Any]
221
+
222
+
223
+ async def get_git_repo_sources(
224
+ url: str,
225
+ commit: str,
226
+ tarball: bool = False,
227
+ ) -> List[_FlatpakSourceType]:
228
+ name = git_repo_name(url, commit)
229
+ if tarball:
230
+ tarball_url = get_git_tarball(url, commit)
231
+ git_repo_sources = [{
232
+ 'type': 'archive',
233
+ 'archive-type': 'tar-gzip',
234
+ 'url': tarball_url,
235
+ 'sha256': await get_remote_sha256(tarball_url),
236
+ 'dest': f'{GIT_CACHE}/{name}',
237
+ }]
238
+ else:
239
+ git_repo_sources = [{
240
+ 'type': 'git',
241
+ 'url': url,
242
+ 'commit': commit,
243
+ 'dest': f'{GIT_CACHE}/{name}',
244
+ }]
245
+ return git_repo_sources
246
+
247
+
248
+ _GitRepo = TypedDict('_GitRepo', {'lock': asyncio.Lock, 'commits': Dict[str, _GitPackagesType]})
249
+ _GitReposType = Dict[str, _GitRepo]
250
+ _VendorEntryType = Dict[str, Dict[str, str]]
251
+
252
+
253
+ async def get_git_package_sources(
254
+ package: _TomlType,
255
+ git_repos: _GitReposType,
256
+ ) -> Tuple[List[_FlatpakSourceType], _VendorEntryType]:
257
+ name = package['name']
258
+ source = package['source']
259
+ commit = urlparse(source).fragment
260
+ assert commit, 'The commit needs to be indicated in the fragement part'
261
+ canonical = canonical_url(source)
262
+ repo_url = canonical.geturl()
263
+
264
+ git_repo = git_repos.setdefault(repo_url, {
265
+ 'commits': {},
266
+ 'lock': asyncio.Lock(),
267
+ })
268
+ async with git_repo['lock']:
269
+ if commit not in git_repo['commits']:
270
+ git_repo['commits'][commit] = await get_git_repo_packages(repo_url, commit)
271
+
272
+ cargo_vendored_entry: _VendorEntryType = {
273
+ repo_url: {
274
+ 'git': repo_url,
275
+ 'replace-with': VENDORED_SOURCES,
276
+ }
277
+ }
278
+ rev = parse_qs(urlparse(source).query).get('rev')
279
+ tag = parse_qs(urlparse(source).query).get('tag')
280
+ branch = parse_qs(urlparse(source).query).get('branch')
281
+ if rev:
282
+ assert len(rev) == 1
283
+ cargo_vendored_entry[repo_url]['rev'] = rev[0]
284
+ elif tag:
285
+ assert len(tag) == 1
286
+ cargo_vendored_entry[repo_url]['tag'] = tag[0]
287
+ elif branch:
288
+ assert len(branch) == 1
289
+ cargo_vendored_entry[repo_url]['branch'] = branch[0]
290
+
291
+ logging.info("Adding package %s from %s", name, repo_url)
292
+ git_pkg = git_repo['commits'][commit][name]
293
+ pkg_repo_dir = os.path.join(GIT_CACHE, git_repo_name(repo_url, commit), git_pkg.path)
294
+ git_sources: List[_FlatpakSourceType] = [
295
+ {
296
+ 'type': 'shell',
297
+ 'commands': [
298
+ f'cp -r --reflink=auto "{pkg_repo_dir}" "{CARGO_CRATES}/{name}"'
299
+ ],
300
+ },
301
+ {
302
+ 'type': 'inline',
303
+ 'contents': toml.dumps(git_pkg.normalized),
304
+ 'dest': f'{CARGO_CRATES}/{name}', #-{version}',
305
+ 'dest-filename': 'Cargo.toml',
306
+ },
307
+ {
308
+ 'type': 'inline',
309
+ 'contents': json.dumps({'package': None, 'files': {}}),
310
+ 'dest': f'{CARGO_CRATES}/{name}', #-{version}',
311
+ 'dest-filename': '.cargo-checksum.json',
312
+ }
313
+ ]
314
+
315
+ return (git_sources, cargo_vendored_entry)
316
+
317
+
318
+ async def get_package_sources(
319
+ package: _TomlType,
320
+ cargo_lock: _TomlType,
321
+ git_repos: _GitReposType,
322
+ ) -> Optional[Tuple[List[_FlatpakSourceType], _VendorEntryType]]:
323
+ metadata = cargo_lock.get('metadata')
324
+ name = package['name']
325
+ version = package['version']
326
+
327
+ if 'source' not in package:
328
+ logging.debug('%s has no source', name)
329
+ return None
330
+ source = package['source']
331
+
332
+ if source.startswith('git+'):
333
+ return await get_git_package_sources(package, git_repos)
334
+
335
+ key = f'checksum {name} {version} ({source})'
336
+ if metadata is not None and key in metadata:
337
+ checksum = metadata[key]
338
+ elif 'checksum' in package:
339
+ checksum = package['checksum']
340
+ else:
341
+ logging.warning(f'{name} doesn\'t have checksum')
342
+ return None
343
+ crate_sources = [
344
+ {
345
+ 'type': 'archive',
346
+ 'archive-type': 'tar-gzip',
347
+ 'url': f'{CRATES_IO}/{name}/{name}-{version}.crate',
348
+ 'sha256': checksum,
349
+ 'dest': f'{CARGO_CRATES}/{name}-{version}',
350
+ },
351
+ {
352
+ 'type': 'inline',
353
+ 'contents': json.dumps({'package': checksum, 'files': {}}),
354
+ 'dest': f'{CARGO_CRATES}/{name}-{version}',
355
+ 'dest-filename': '.cargo-checksum.json',
356
+ },
357
+ ]
358
+ return (crate_sources, {'crates-io': {'replace-with': VENDORED_SOURCES}})
359
+
360
+
361
+ async def generate_sources(
362
+ cargo_lock: _TomlType,
363
+ git_tarballs: bool = False,
364
+ ) -> List[_FlatpakSourceType]:
365
+ # {
366
+ # "git-repo-url": {
367
+ # "lock": asyncio.Lock(),
368
+ # "commits": {
369
+ # "commit-hash": {
370
+ # "package-name": "./relative/package/path"
371
+ # }
372
+ # }
373
+ # }
374
+ # }
375
+ git_repos: _GitReposType = {}
376
+ sources: List[_FlatpakSourceType] = []
377
+ package_sources = []
378
+ cargo_vendored_sources = {
379
+ VENDORED_SOURCES: {'directory': f'{CARGO_CRATES}'},
380
+ }
381
+
382
+ pkg_coros = [get_package_sources(p, cargo_lock, git_repos) for p in cargo_lock['package']]
383
+ for pkg in await asyncio.gather(*pkg_coros):
384
+ if pkg is None:
385
+ continue
386
+ else:
387
+ pkg_sources, cargo_vendored_entry = pkg
388
+ package_sources.extend(pkg_sources)
389
+ cargo_vendored_sources.update(cargo_vendored_entry)
390
+
391
+ logging.debug('Adding collected git repos:\n%s', json.dumps(list(git_repos), indent=4))
392
+ git_repo_coros = []
393
+ for git_url, git_repo in git_repos.items():
394
+ for git_commit in git_repo['commits']:
395
+ git_repo_coros.append(get_git_repo_sources(git_url, git_commit, git_tarballs))
396
+ sources.extend(sum(await asyncio.gather(*git_repo_coros), []))
397
+
398
+ sources.extend(package_sources)
399
+
400
+ logging.debug('Vendored sources:\n%s', json.dumps(cargo_vendored_sources, indent=4))
401
+ sources.append({
402
+ 'type': 'inline',
403
+ 'contents': toml.dumps({
404
+ 'source': cargo_vendored_sources,
405
+ }),
406
+ 'dest': CARGO_HOME,
407
+ 'dest-filename': 'config'
408
+ })
409
+ return sources
410
+
411
+
412
+ def main():
413
+ parser = argparse.ArgumentParser()
414
+ parser.add_argument('cargo_lock', help='Path to the Cargo.lock file')
415
+ parser.add_argument('-o', '--output', required=False, help='Where to write generated sources')
416
+ parser.add_argument('-t', '--git-tarballs', action='store_true', help='Download git repos as tarballs')
417
+ parser.add_argument('-d', '--debug', action='store_true')
418
+ args = parser.parse_args()
419
+ if args.output is not None:
420
+ outfile = args.output
421
+ else:
422
+ outfile = 'generated-sources.json'
423
+ if args.debug:
424
+ loglevel = logging.DEBUG
425
+ else:
426
+ loglevel = logging.INFO
427
+ logging.basicConfig(level=loglevel)
428
+
429
+ generated_sources = asyncio.run(generate_sources(load_toml(args.cargo_lock),
430
+ git_tarballs=args.git_tarballs))
431
+ with open(outfile, 'w') as out:
432
+ json.dump(generated_sources, out, indent=4, sort_keys=False)
433
+
434
+
435
+ if __name__ == '__main__':
436
+ main()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: flatpak-cargo-generator
3
+ Version: 0.1.0
4
+ Summary: Unofficial Pypi distribution of the Flatpak's cargo dependency generator
5
+ Author: Flatpak team
6
+ Requires-Python: >=3.8
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: Unix
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
16
+ Requires-Dist: toml (>=0.10.2,<0.11.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Flatpak Cargo Generator
20
+
21
+ This is an unofficial Pypi distribution of the [flatpak-cargo-generator](https://github.com/flatpak/flatpak-builder-tools/blob/master/cargo/flatpak-cargo-generator.py) script, ensuring library dependencies are installed and the script is available on your PATH as `flatpak-cargo-generator`.
22
+
23
+ See [here](https://github.com/flatpak/flatpak-builder-tools/blob/master/cargo/README.md) for instructions.
24
+
@@ -0,0 +1,7 @@
1
+ flatpak_cargo_generator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ flatpak_cargo_generator/script.py,sha256=vhVdAIOceBxdMXaJH6yrLls6Qj6Ixzx3maa35dOjeG0,14572
3
+ flatpak_cargo_generator-0.1.0.dist-info/LICENSE.md,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
4
+ flatpak_cargo_generator-0.1.0.dist-info/METADATA,sha256=PKu6swku8_EnIJQNB1WBq3aG6PvpTMIdtzeJOBTt2qM,1129
5
+ flatpak_cargo_generator-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
6
+ flatpak_cargo_generator-0.1.0.dist-info/entry_points.txt,sha256=1EgZu8-TzvS_4qBfk20PNVYewKNQlCr4EHkaCdVPU_o,79
7
+ flatpak_cargo_generator-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ flatpak-cargo-generator=flatpak_cargo_generator.script:main
3
+