Skip to content

Commit 2cc8dd0

Browse files
Ian2020dbrgnjwillikers
authored andcommitted
node: Add support for git sources
Co-authored-by: Danilo Bargen <[email protected]> Co-authored-by: Jordan Williams <[email protected]>
1 parent f8e2d1c commit 2cc8dd0

File tree

14 files changed

+862
-90
lines changed

14 files changed

+862
-90
lines changed

node/flatpak_node_generator/package.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,25 @@ class GitSource(PackageSource):
125125
from_: Optional[str]
126126

127127

128+
@dataclass(frozen=True, eq=True)
129+
class NamedGitSource:
130+
package_name: str
131+
git_source: GitSource
132+
133+
128134
@dataclass(frozen=True, eq=True)
129135
class LocalSource(PackageSource):
130136
path: str
131137

132138

139+
@dataclass(frozen=True, eq=True)
140+
class Lockfile:
141+
path: Path
142+
version: int
143+
144+
133145
class Package(NamedTuple):
134146
name: str
135147
version: str
136148
source: PackageSource
137-
lockfile: Path
149+
lockfile: Lockfile

node/flatpak_node_generator/providers/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
'git': {},
1616
'git+http': {'scheme': 'http'},
1717
'git+https': {'scheme': 'https'},
18+
'git+ssh': {'scheme': 'https'},
1819
}
1920

2021

@@ -32,6 +33,14 @@ def parse_git_source(self, version: str, from_: Optional[str] = None) -> GitSour
3233
if not new_url.netloc:
3334
path = new_url.path.split('/')
3435
new_url = new_url._replace(netloc=path[0], path='/'.join(path[1:]))
36+
# Replace https://[email protected]:ianstormtaylor/to-camel-case.git
37+
# with https://[email protected]/ianstormtaylor/to-camel-case.git
38+
# for git+ssh URLs
39+
if ':' in new_url.netloc:
40+
netloc_split = new_url.netloc.split(':', 1)
41+
new_url = new_url._replace(
42+
netloc=netloc_split[0], path=f'/{netloc_split[1]}{new_url.path}'
43+
)
3544

3645
return GitSource(
3746
original=original_url.geturl(),
@@ -40,7 +49,7 @@ def parse_git_source(self, version: str, from_: Optional[str] = None) -> GitSour
4049
from_=from_,
4150
)
4251

43-
def process_lockfile(self, lockfile: Path) -> Iterator[Package]:
52+
def process_lockfile(self, lockfile_path: Path) -> Iterator[Package]:
4453
raise NotImplementedError()
4554

4655

node/flatpak_node_generator/providers/npm.py

Lines changed: 135 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from ..package import (
2626
GitSource,
2727
LocalSource,
28+
Lockfile,
29+
NamedGitSource,
2830
Package,
2931
PackageSource,
3032
PackageURLSource,
@@ -52,7 +54,7 @@ def __init__(self, options: Options):
5254
self.no_devel = options.no_devel
5355

5456
def _process_packages_v1(
55-
self, lockfile: Path, entry: Dict[str, Dict[Any, Any]]
57+
self, lockfile: Lockfile, entry: Dict[str, Dict[Any, Any]]
5658
) -> Iterator[Package]:
5759
for pkgname, info in entry.get('dependencies', {}).items():
5860
if info.get('dev') and self.no_devel:
@@ -81,23 +83,33 @@ def _process_packages_v1(
8183
elif version_url.scheme == 'file':
8284
source = LocalSource(path=version_url.path)
8385
else:
84-
integrity = Integrity.parse(info['integrity'])
86+
integrity = None
87+
if 'integrity' in info:
88+
integrity = Integrity.parse(info['integrity'])
89+
8590
if 'resolved' in info:
86-
source = ResolvedSource(
87-
resolved=info['resolved'], integrity=integrity
88-
)
91+
resolved = info['resolved']
92+
if resolved.startswith('git+'):
93+
source = self.parse_git_source(resolved)
94+
else:
95+
source = ResolvedSource(resolved=resolved, integrity=integrity)
8996
elif version_url.scheme in {'http', 'https'}:
9097
source = PackageURLSource(resolved=version, integrity=integrity)
9198
else:
9299
source = RegistrySource(integrity=integrity)
93100

94-
yield Package(name=name, version=version, source=source, lockfile=lockfile)
101+
yield Package(
102+
name=name,
103+
version=version,
104+
source=source,
105+
lockfile=lockfile,
106+
)
95107

96108
if 'dependencies' in info:
97109
yield from self._process_packages_v1(lockfile, info)
98110

99111
def _process_packages_v2(
100-
self, lockfile: Path, entry: Dict[str, Dict[Any, Any]]
112+
self, lockfile: Lockfile, entry: Dict[str, Dict[Any, Any]]
101113
) -> Iterator[Package]:
102114
for install_path, info in entry.get('packages', {}).items():
103115
if (info.get('dev') or info.get('devOptional')) and self.no_devel:
@@ -127,14 +139,10 @@ def _process_packages_v2(
127139
integrity=integrity, resolved=info['resolved']
128140
)
129141
elif resolved_url.scheme.startswith('git+'):
130-
raise NotImplementedError(
131-
'Git sources in lockfile v2 format are not supported yet'
132-
f' (package {install_path} in {lockfile})'
133-
)
142+
source = self.parse_git_source(info['resolved'])
134143
else:
135-
raise NotImplementedError(
136-
f"Don't know how to handle package {install_path} in {lockfile}"
137-
)
144+
source = LocalSource(path=install_path)
145+
name = install_path
138146

139147
# NOTE We can't reliably determine the package name from the lockfile v2 syntax,
140148
# but we need it for registry queries and special source processing;
@@ -151,20 +159,20 @@ def _process_packages_v2(
151159
source=source,
152160
)
153161

154-
def process_lockfile(self, lockfile: Path) -> Iterator[Package]:
155-
with open(lockfile, encoding='utf-8') as fp:
162+
def process_lockfile(self, lockfile_path: Path) -> Iterator[Package]:
163+
with open(lockfile_path, encoding='utf-8') as fp:
156164
data = json.load(fp)
157165

166+
lockfile = Lockfile(lockfile_path, data['lockfileVersion'])
167+
158168
# TODO Once lockfile v2 syntax support is complete, use _process_packages_v2
159169
# for both v2 and v2 lockfiles
160-
if data['lockfileVersion'] in {1, 2}:
170+
if lockfile.version in {1, 2}:
161171
yield from self._process_packages_v1(lockfile, data)
162-
elif data['lockfileVersion'] in {3}:
172+
elif lockfile.version in {3}:
163173
yield from self._process_packages_v2(lockfile, data)
164174
else:
165-
raise NotImplementedError(
166-
f'Unknown lockfile version {data["lockfileVersion"]}'
167-
)
175+
raise NotImplementedError(f'Unknown lockfile version {lockfile.version}')
168176

169177

170178
class NpmRCFileProvider(RCFileProvider):
@@ -202,9 +210,10 @@ def __init__(
202210
str, asyncio.Future[NpmModuleProvider.RegistryPackageIndex]
203211
] = {}
204212
self.index_entries: Dict[Path, str] = {}
205-
self.all_lockfiles: Set[Path] = set()
206-
# Mapping of lockfiles to a dict of the Git source target paths and GitSource objects.
207-
self.git_sources: DefaultDict[Path, Dict[Path, GitSource]] = (
213+
self.all_lockfiles: Set[Lockfile] = set()
214+
# Mapping of lockfiles to a dict of the Git source target paths and
215+
# NamedGitSource objects (package name + GitSource)
216+
self.git_sources: DefaultDict[Lockfile, Dict[Path, NamedGitSource]] = (
208217
collections.defaultdict(lambda: {})
209218
)
210219
# FIXME better pass the same provider object we created in main
@@ -369,9 +378,54 @@ async def generate_package(self, package: Package) -> None:
369378
# Get a unique name to use for the Git repository folder.
370379
name = f'{package.name}-{source.commit}'
371380
path = self.gen.data_root / 'git-packages' / name
372-
self.git_sources[package.lockfile][path] = source
381+
self.git_sources[package.lockfile][path] = NamedGitSource(
382+
package.name, source
383+
)
373384
self.gen.add_git_source(source.url, source.commit, path)
374385

386+
git_suffix = re.compile(r'\.git$')
387+
url = urllib.parse.urlparse(source.url)
388+
389+
if url.hostname == 'github.com':
390+
url = url._replace(
391+
netloc='codeload.github.com',
392+
path=git_suffix.sub('', url.path),
393+
)
394+
tarball_url = url._replace(path=url.path + f'/tar.gz/{source.commit}')
395+
index_url = tarball_url.geturl()
396+
elif url.hostname == 'gitlab.com':
397+
url = url._replace(
398+
netloc='gitlab.com', path=git_suffix.sub('', url.path)
399+
)
400+
tarball_url = url._replace(
401+
path=url.path
402+
+ f'/-/archive/{source.commit}/{package.name}-{source.commit}.tar.gz'
403+
)
404+
index_url = url._replace(
405+
path=url.path + f'/repository/archive.tar.gz?ref={source.commit}'
406+
).geturl()
407+
else:
408+
raise NotImplementedError(
409+
f"Don't know how to handle git source with url {url.geturl()}"
410+
)
411+
412+
metadata = await RemoteUrlMetadata.get(
413+
tarball_url.geturl(),
414+
cachable=True,
415+
integrity_algorithm='sha512',
416+
)
417+
418+
self.gen.add_url_source(
419+
url=tarball_url.geturl(),
420+
integrity=metadata.integrity,
421+
destination=self.get_cacache_content_path(metadata.integrity),
422+
)
423+
424+
self.add_index_entry(
425+
url=index_url,
426+
metadata=metadata,
427+
)
428+
375429
elif isinstance(source, LocalSource):
376430
pass
377431

@@ -393,7 +447,7 @@ def get_lockfile_rc(self, lockfile: Path) -> Dict[str, str]:
393447

394448
def get_package_registry(self, package: Package) -> str:
395449
assert isinstance(package.source, RegistrySource)
396-
rc = self.get_lockfile_rc(package.lockfile)
450+
rc = self.get_lockfile_rc(package.lockfile.path)
397451
if rc and '/' in package.name:
398452
scope, _ = package.name.split('/', maxsplit=1)
399453
if f'{scope}:registry' in rc:
@@ -432,8 +486,8 @@ def _finalize(self) -> None:
432486
if type == "object"
433487
then
434488
to_entries | map(
435-
if (.value | type == "string") and $data[.value]
436-
then .value = "git+file:\($buildroot)/\($data[.value])"
489+
if (.key | type == "string") and $data[.key]
490+
then .value = "git+file://\($buildroot)/\($data[.key])"
437491
else .
438492
end
439493
) | from_entries
@@ -445,70 +499,79 @@ def _finalize(self) -> None:
445499
walk(
446500
if type == "object" and (.version | type == "string") and $data[.version]
447501
then
448-
.version = "git+file:\($buildroot)/\($data[.version])"
502+
.resolved = "git+file:\($buildroot)/\($data[.version])"
449503
else .
450504
end
451505
)
452506
""",
453507
}
454508

455509
for lockfile, sources in self.git_sources.items():
456-
prefix = self.relative_lockfile_dir(lockfile)
510+
prefix = self.relative_lockfile_dir(lockfile.path)
457511
data: Dict[str, Dict[str, str]] = {
458512
'package.json': {},
459513
'package-lock.json': {},
460514
}
461515

462-
for path, source in sources.items():
463-
GIT_URL_PREFIX = 'git+'
464-
465-
new_version = f'{path}#{source.commit}'
466-
assert source.from_ is not None
467-
data['package.json'][source.from_] = new_version
468-
data['package-lock.json'][source.original] = new_version
469-
470-
if source.from_.startswith(GIT_URL_PREFIX):
471-
data['package.json'][source.from_[len(GIT_URL_PREFIX) :]] = (
516+
if lockfile.version == 1:
517+
for path, named_git_source in sources.items():
518+
GIT_URL_PREFIX = 'git+'
519+
new_version = f'{path}#{named_git_source.git_source.commit}'
520+
data['package.json'][named_git_source.package_name] = (
472521
new_version
473522
)
474-
475-
if source.original.startswith(GIT_URL_PREFIX):
476523
data['package-lock.json'][
477-
source.original[len(GIT_URL_PREFIX) :]
524+
named_git_source.git_source.original
478525
] = new_version
479526

480-
for filename, script in scripts.items():
481-
target = Path('$FLATPAK_BUILDER_BUILDDIR') / prefix / filename
482-
processed_script = (
483-
textwrap.dedent(script.lstrip('\n')).strip().replace('\n', '')
484-
)
485-
json_data = json.dumps(data[filename])
486-
patch_commands[lockfile].append(
487-
'jq'
488-
' --arg buildroot "$FLATPAK_BUILDER_BUILDDIR"'
489-
f' --argjson data {shlex.quote(json_data)}'
490-
f' {shlex.quote(processed_script)} {target}'
491-
f' > {target}.new'
492-
)
493-
patch_commands[lockfile].append(f'mv {target}{{.new,}}')
494-
495-
patch_all_commands: List[str] = []
496-
for lockfile in self.all_lockfiles:
497-
patch_dest = (
498-
self.gen.data_root / 'patch' / self.relative_lockfile_dir(lockfile)
499-
)
500-
# Don't use with_extension to avoid problems if the package has a . in its name.
501-
patch_dest = patch_dest.with_name(patch_dest.name + '.sh')
527+
if named_git_source.git_source.original.startswith(
528+
GIT_URL_PREFIX
529+
):
530+
data['package-lock.json'][
531+
named_git_source.git_source.original[
532+
len(GIT_URL_PREFIX) :
533+
]
534+
] = new_version
535+
536+
for filename, script in scripts.items():
537+
target = Path('$FLATPAK_BUILDER_BUILDDIR') / prefix / filename
538+
minified_script = (
539+
textwrap.dedent(script.lstrip('\n'))
540+
.strip()
541+
.replace('\n', '')
542+
)
543+
json_data = json.dumps(data[filename])
544+
patch_commands[lockfile.path].append(
545+
'jq'
546+
' --arg buildroot "$FLATPAK_BUILDER_BUILDDIR"'
547+
f' --argjson data {shlex.quote(json_data)}'
548+
f' {shlex.quote(minified_script)} {target}'
549+
f' > {target}.new'
550+
)
551+
patch_commands[lockfile.path].append(f'mv {target}{{.new,}}')
552+
553+
if len(patch_commands) > 0:
554+
patch_all_commands: List[str] = []
555+
for lockfile in self.all_lockfiles:
556+
patch_dest = (
557+
self.gen.data_root
558+
/ 'patch'
559+
/ self.relative_lockfile_dir(lockfile.path)
560+
)
561+
# Don't use with_extension to avoid problems if the package has a . in its name.
562+
patch_dest = patch_dest.with_name(patch_dest.name + '.sh')
502563

503-
self.gen.add_script_source(patch_commands[lockfile], patch_dest)
504-
patch_all_commands.append(f'"$FLATPAK_BUILDER_BUILDDIR"/{patch_dest}')
564+
self.gen.add_script_source(patch_commands[lockfile.path], patch_dest)
565+
patch_all_commands.append(f'"$FLATPAK_BUILDER_BUILDDIR"/{patch_dest}')
505566

506-
patch_all_dest = self.gen.data_root / 'patch-all.sh'
507-
self.gen.add_script_source(patch_all_commands, patch_all_dest)
567+
patch_all_dest = self.gen.data_root / 'patch-all.sh'
568+
self.gen.add_script_source(patch_all_commands, patch_all_dest)
508569

509-
if not self.no_autopatch:
510-
# FLATPAK_BUILDER_BUILDDIR isn't defined yet for script sources.
511-
self.gen.add_command(f'FLATPAK_BUILDER_BUILDDIR="$PWD" {patch_all_dest}')
570+
if not self.no_autopatch:
571+
# FLATPAK_BUILDER_BUILDDIR isn't defined yet for script sources.
572+
self.gen.add_command(
573+
f'FLATPAK_BUILDER_BUILDDIR="$PWD" {patch_all_dest}'
574+
)
512575

513576
if self.index_entries:
514577
for path, entry in self.index_entries.items():

0 commit comments

Comments
 (0)