Skip to content

Commit 9eb54ff

Browse files
committed
Add flakeref support #201
* Correct typo * Add tests Signed-off-by: Chin Yeung Li <tli@nexb.com>
1 parent 21f8c9a commit 9eb54ff

2 files changed

Lines changed: 135 additions & 16 deletions

File tree

src/fetchcode/nix.py

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def get_download_url(cls, purl):
7171
name = purl_data.name
7272
version = purl_data.version
7373
if not version:
74-
raise Exception("Version is requierd.")
74+
raise Exception("Version is required.")
7575
qualifiers = purl_data.qualifiers or {}
7676

7777
if "system" in qualifiers:
@@ -130,28 +130,30 @@ def get_upstream_src_download_url(cls, purl):
130130
if shutil.which("nix") is not None:
131131
have_nix = True
132132

133-
namespace = purl_data.namespace
134-
# We will only work with the official nixpkgs repository, at least
135-
# for now.
136-
if not namespace or namespace.lower() != "nixpkgs":
137-
raise Exception(
138-
"Only official nixpkgs repository is supported (i.e. namespace=nixpkgs)."
139-
)
140133
name = purl_data.name
134+
namespace = purl_data.namespace
141135
version = purl_data.version
136+
qualifiers = purl_data.qualifiers or {}
137+
commit_hash = qualifiers.get("commit", "")
138+
flakeref = qualifiers.get("flakeref", "")
139+
142140
if not version:
143141
raise Exception("Version is requierd.")
144-
data = cls.get_package_data(purl)
142+
143+
if namespace != "nixpkgs" and not flakeref:
144+
raise Exception(
145+
"Only official nixpkgs repository is supported, "
146+
"or please provide the flakeref qualifier."
147+
)
148+
data = cls.get_package_data(purl) if not flakeref else None
145149

146150
if data:
147151
download_url = construct_url_based_on_homepage_url(purl_data, data)
148152

149153
if not download_url and have_nix:
150-
download_url = retrieve_src_download_url_with_nix(name, version)
151-
if not download_url and data and version:
154+
if not commit_hash and data:
152155
commit_hash = get_commit_hash(data, version)
153-
if commit_hash:
154-
download_url = retrieve_src_download_url_with_nix(name, version, commit_hash)
156+
download_url = retrieve_src_download_url_with_nix(name, version, commit_hash, flakeref)
155157

156158
if not download_url and not have_nix:
157159
print("Install `nix` and re-run to let `nix` determine the download URL.")
@@ -208,11 +210,11 @@ def get_nix_store_path_with_nix(name, system, output, commit_hash):
208210
return None
209211

210212

211-
def retrieve_src_download_url_with_nix(name, version=None, commit_hash=None):
213+
def retrieve_src_download_url_with_nix(name, version, commit_hash=None, flakeref=None):
212214
"""
213215
Find and return the source download url using 'nix'
214216
"""
215-
info = get_src_info(name, commit_hash)
217+
info = get_src_info(name, version, commit_hash, flakeref)
216218
urls = info.get("urls", [])
217219

218220
download_url = None
@@ -260,14 +262,35 @@ def retrieve_src_download_url_with_nix(name, version=None, commit_hash=None):
260262
return download_url
261263

262264

263-
def get_src_info(attr_path, commit_hash=None):
265+
def get_src_info(attr_path, version, commit_hash=None, flakeref=None):
264266
"""
265267
Use the `nix-instantiate` command together with the nix_expression to
266268
retrieve the package’s version and download URL. Return a dictionary
267269
with "version" and "urls" keys.
268270
"""
269271
config_str = "config = { allowBroken = true; allowUnfree = true; };"
270272

273+
if flakeref:
274+
if not flakeref.startswith("github"):
275+
print("Only flakeref for github is supported at the moment.")
276+
return {"version": None, "urls": []}
277+
278+
rest = flakeref.partition(":")[2]
279+
parts = rest.split("/")
280+
if len(parts) < 2:
281+
return {"version": None, "urls": []}
282+
owner, repo = parts[0], parts[1]
283+
git_repo = f"https://github.com/{owner}/{repo}.git"
284+
285+
version_tag = version
286+
if not commit_hash:
287+
commit_hash, version_tag = get_flakeref_version_commit_hash(git_repo, version)
288+
if commit_hash and version_tag:
289+
url = f"https://github.com/{owner}/{repo}/archive/{commit_hash}.tar.gz"
290+
return {"version": version_tag, "urls": [url]}
291+
else:
292+
return {"version": None, "urls": []}
293+
271294
# Determine the repository entry point definition
272295
if commit_hash:
273296
nixpkgs_import = (
@@ -308,6 +331,44 @@ def get_src_info(attr_path, commit_hash=None):
308331
return {"version": None, "urls": []}
309332

310333

334+
def get_flakeref_version_commit_hash(git_repo, version):
335+
"""
336+
Get the commit hash from the given version.
337+
"""
338+
if shutil.which("git") is None:
339+
return None, None
340+
cmd = [
341+
"git",
342+
"ls-remote",
343+
"--tags",
344+
git_repo,
345+
]
346+
try:
347+
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=300)
348+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
349+
print(f"Error running git ls-remote for {git_repo}: {e}", file=sys.stderr)
350+
return None, None
351+
352+
potential_version_matches = [
353+
version,
354+
f"v{version}",
355+
f"V{version}",
356+
f"v-{version}",
357+
f"V-{version}",
358+
f"release-{version}",
359+
f"RELEASE-{version}",
360+
]
361+
for line in result.stdout.splitlines():
362+
parts = line.split("\t")
363+
remote_hash = parts[0]
364+
reference_tag = parts[1]
365+
version_tag = reference_tag.replace("refs/tags/", "").replace("^{}", "")
366+
367+
if version_tag in potential_version_matches:
368+
return remote_hash, version_tag
369+
return None, None
370+
371+
311372
def construct_url_based_on_homepage_url(input_purl, data):
312373
"""
313374
Determine and return the download url based on the homepage and

tests/test_nix.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,61 @@ def test_get_version_from_commit_hash(self, mock_fetch):
192192
mock_data, "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9"
193193
)
194194
self.assertEqual(version, "2.6")
195+
196+
@patch("fetchcode.nix.get_flakeref_version_commit_hash")
197+
def test_get_src_info_with_flakeref_no_commit_hash(self, mock_get_flake_hash, mock_fetch):
198+
mock_get_flake_hash.return_value = ("aaaaaaaaa", "v2.0")
199+
200+
result = nix.get_src_info("package_name", "2.0", flakeref="github:owner/repo")
201+
202+
self.assertEqual(result["version"], "v2.0")
203+
self.assertEqual(
204+
result["urls"][0], "https://github.com/owner/repo/archive/aaaaaaaaa.tar.gz"
205+
)
206+
mock_get_flake_hash.assert_called_once_with("https://github.com/owner/repo.git", "2.0")
207+
208+
def test_get_src_info_with_flakeref_and_commit_hash(self, mock_fetch):
209+
result = nix.get_src_info(
210+
"package_name", version="2.0", commit_hash="aaaaaaaaa", flakeref="github:owner/repo"
211+
)
212+
213+
self.assertEqual(result["version"], "2.0")
214+
self.assertEqual(
215+
result["urls"][0], "https://github.com/owner/repo/archive/aaaaaaaaa.tar.gz"
216+
)
217+
218+
@patch("subprocess.run")
219+
@patch("shutil.which")
220+
def test_get_flakeref_version_commit_hash_success(
221+
self, mock_which, mock_subproc_run, mock_fetch
222+
):
223+
mock_which.return_value = "/usr/bin/git"
224+
225+
mock_subproc_run.return_value.stdout = (
226+
"1111111111111111111111111111111111111111\trefs/tags/1.0\n"
227+
"2222222222222222222222222222222222222222\trefs/tags/v2.0\n"
228+
"2222222222222222222222222222222222222222\trefs/tags/v3.0\n"
229+
)
230+
231+
commit_hash, version_tag = nix.get_flakeref_version_commit_hash(
232+
"https://github.com/owner/repo.git", "2.0"
233+
)
234+
235+
self.assertEqual(commit_hash, "2222222222222222222222222222222222222222")
236+
self.assertEqual(version_tag, "v2.0")
237+
238+
@patch("subprocess.run")
239+
@patch("shutil.which")
240+
def test_get_flakeref_version_commit_hash_no_match(
241+
self, mock_which, mock_subproc_run, mock_fetch
242+
):
243+
mock_which.return_value = "/usr/bin/git"
244+
mock_subproc_run.return_value.stdout = (
245+
"1111111111111111111111111111111111111111\trefs/tags/1.0\n"
246+
)
247+
248+
commit_hash, version_tag = nix.get_flakeref_version_commit_hash(
249+
"https://github.com/owner/repo.git", "1.1"
250+
)
251+
self.assertIsNone(commit_hash)
252+
self.assertIsNone(version_tag)

0 commit comments

Comments
 (0)