@@ -1292,7 +1292,7 @@ class FetchData(object):
elif checksum_plain_name in self.parm:
checksum_expected = self.parm[checksum_plain_name]
checksum_name = checksum_plain_name
- elif self.type not in ["http", "https", "ftp", "ftps", "sftp", "s3", "az", "crate", "gs", "gomod", "npm"]:
+ elif self.type not in ["http", "https", "ftp", "ftps", "sftp", "s3", "az", "crate", "gs", "gomod", "npm", "ghra"]:
checksum_expected = None
else:
checksum_expected = d.getVarFlag("SRC_URI", checksum_name)
@@ -2068,6 +2068,7 @@ from . import az
from . import crate
from . import gcp
from . import gomod
+from . import github_release_artifact
methods.append(local.Local())
methods.append(wget.Wget())
@@ -2092,3 +2093,4 @@ methods.append(crate.Crate())
methods.append(gcp.GCP())
methods.append(gomod.GoMod())
methods.append(gomod.GoModGit())
+methods.append(github_release_artifact.GitHubReleaseArtifact())
new file mode 100644
@@ -0,0 +1,91 @@
+"""
+BitBake 'Fetch' GitHub release artifacts implementation
+
+"""
+
+# Copyright (C) 2025 Leonard Göhrs
+#
+# Based on bb.fetch2.wget:
+# Copyright (C) 2003, 2004 Chris Larson
+#
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Based on functions from the base bb module, Copyright 2003 Holger Schurig
+
+import json
+
+from urllib.request import urlopen, Request
+
+from bb.fetch2 import FetchError
+from bb.fetch2.wget import Wget
+
+
+class GitHubReleaseArtifact(Wget):
+ API_HEADERS = {
+ "Accept": "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+ }
+
+ DOWNLOAD_HEADERS = {
+ "Accept": "application/octet-stream"
+ }
+
+ def supports(self, ud, d):
+ return ud.type in ["ghra"]
+
+ def _resolve_artifact_url(self, ud, d):
+ """Resolve `ghra://` pseudo URLs to `https://` URLs and set auth header.
+
+ This method resolved URLs like `ghra://github.com/rauc/rauc/v1.13/rauc-1.13.tar.xz`
+ to a backing URL like `https://api.github.com/repos/rauc/rauc/releases/assets/222455085`
+ while optionally setting the required authentication headers to download from
+ private repositories.
+ """
+
+ try:
+ user, repo, tag, asset_name = ud.path.strip("/").split("/")
+ except ValueError as e:
+ raise FetchError(
+ f"Expected path like '/<user>/<repo>/<tag>/<asset_name>', got: '{ud.path}'"
+ ) from e
+
+ meta_url = f"https://api.{ud.host}/repos/{user}/{repo}/releases/tags/{tag}"
+
+ auth_headers = {}
+
+ token = d.getVar("BB_FETCH_GHRA_TOKEN")
+
+ if token is not None:
+ auth_headers["Authorization"] = f"Bearer {token}"
+
+ try:
+ headers = dict(**auth_headers, **self.API_HEADERS)
+ req = Request(url=meta_url, headers=headers)
+ with urlopen(req) as resp:
+ result = json.load(resp)
+
+ except Exception as e:
+ raise FetchError(f"Error downloading artifact list: {e}") from e
+
+ asset_urls = dict((asset["name"], asset["url"]) for asset in result["assets"])
+
+ if asset_name not in asset_urls:
+ asset_list = ", ".join(asset_urls.keys())
+ raise FetchError(
+ f"Did not find asset '{asset_name}' in release asset list: {asset_list}"
+ )
+
+ # Override the `url` and `http_headers` in the FetchData object,
+ # enabling the Wget class to perform the actual downloading.
+ ud.url = asset_urls[asset_name]
+ ud.http_headers = dict(**auth_headers, **self.DOWNLOAD_HEADERS)
+
+ def checkstatus(self, fetch, ud, d):
+ self._resolve_artifact_url(ud, d)
+
+ return super().checkstatus(fetch, ud, d)
+
+ def download(self, ud, d):
+ self._resolve_artifact_url(ud, d)
+
+ return super().download(ud, d)
This fetcher enables downloading artifacts attached to GitHub releases in _private repositories_ (public repositories can just use download URLs like `https://github.com/rauc/rauc/releases/download/v1.13/rauc-1.13.tar.xz` which work without authentication). The `SRC_URI` includes the Github account and repository (`rauc/rauc`), git tag of the release (`v1.13`) and the name of the artifact file (`rauc-1.13.tar.xz`): SRC_URI = "ghra://github.com/rauc/rauc/v1.13/rauc-1.13.tar.xz" SRC_URI[sha256sum] = "1ddb218a5d713c8dbd6e04d5501d96629f1c8e252157... Authentication is provided using tokens, for example by configuring them in the `local.conf`¹: $ grep TOKEN build/conf/local.conf BB_FETCH_GHRA_TOKEN = "github_pat_123456789abc... The token may also be provided using an environment variable, e.g. for CI builds: $ export BB_ENV_PASSTHROUGH_ADDITIONS="BB_FETCH_GHRA_TOKEN" $ export BB_FETCH_GHRA_TOKEN="github_pat_123456789abc... $ bitbake rauc or by setting `BB_FETCH_GHRA_TOKEN` inside the recipe: BB_FETCH_GHRA_TOKEN = "github_pat_123456789abc... SRC_URI = "ghra://github.com/rauc/rauc/v1.13/rauc-1.13.tar.xz" SRC_URI[sha256sum] = "1ddb218a5d713c8dbd6e04d5501d96629f1c8e252157... ¹ Note: When using a personal access token, it should be restricted in scope to only allow downloading of release artifacts for the required repositories. Signed-off-by: Leonard Göhrs <l.goehrs@pengutronix.de> --- lib/bb/fetch2/__init__.py | 4 +- lib/bb/fetch2/github_release_artifact.py | 91 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 lib/bb/fetch2/github_release_artifact.py