From patchwork Wed Jul 22 08:51:35 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Turull X-Patchwork-Id: 93137 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 0C353C4451C for ; Wed, 22 Jul 2026 08:52:03 +0000 (UTC) Received: from GVXPR05CU001.outbound.protection.outlook.com (GVXPR05CU001.outbound.protection.outlook.com [52.101.83.32]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.43525.1784710318991738813 for ; Wed, 22 Jul 2026 01:51:59 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: body hash did not verify" header.i=@ericsson.com header.s=selector2 header.b=VVmsvOHQ; spf=pass (domain: ericsson.com, ip: 52.101.83.32, mailfrom: edaturu@ericsson.com) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=xzuFGwytbl4Pg3dZLmKLOGOHioK4qZh4oesFwzx1gu/zVPI0v+u1yBA/ZrRGk/qAbhfyq+rjGW9TehQAqW6+0to8SLBwwSHN5PuWMendth0vHiQSCf7fR3RcsVw0GoWFS6s3yezisnCpFAS29vZ2T+fQiYYdBp1699egB74+b04TpZzAfhYM8PJS5oFXpDfA52eK33dmKGzx9oRpNtc6e/rWlSGQwVij7JUgnhv0jKj6MxNTqMAhZN2KHog31Re/XoBFxTOuR/cNH7hXIFn0N1BzLz0o+TuZE00YKAOh2kvPN01MOpEw4dauUcikXAZEbc3kKUdfJjPT1JzaiefvJw== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=abJAUPM4sTEMk1E90c2LdKTIMQ+y0/w53gy59zd8U+Q=; b=hYPwUVD58aaZask33WwMezOUx6k80qpKf8YYUxin7J6nF/RIZEVYPaxE4Yat1m8VMLNF4lQMvMsPZIN1CVBjaR1p2ZYs3M8gEmZQkO0ZvZFmcnvjF9+ncVk+dZhj5OSBkAy8sTniG7fLu4Vrx9+qijletZC6KSnMtWOycfObCxsbEb7Wjdf78/dp5u6N+KqDUmc4xFVDiri5YjAe7DsMPQf3XzUcP4zTkXgPfOXtyYCbSXjy1yfPWgBOoU1HgcL3krSD1/ysTx5tYW1UHkOg7N2Kp5MQ5mO+2htSW0w97rvadO78BdzctW4k3qMajVAjPtCFx8u11FjmBIDAeG1qiQ== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass (sender ip is 192.176.1.74) smtp.rcpttodomain=lists.openembedded.org smtp.mailfrom=ericsson.com; dmarc=pass (p=reject sp=reject pct=100) action=none header.from=ericsson.com; dkim=none (message not signed); arc=none (0) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ericsson.com; s=selector2; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=abJAUPM4sTEMk1E90c2LdKTIMQ+y0/w53gy59zd8U+Q=; b=VVmsvOHQCZJa8JcRoTGDxuWP0WO2ZuPkO8lUBjtOj7eLIq+yOn5oaVJDa+g4C7tFER0Op9LBufBTzGcW09dbh6kspW6/buVkJWh5/E8omKsAelkpwxfqYhjvcBYOEO4MSF3Dycq5SLZSqkBeHWC2wMy/gJWoKBqaYwQY34uxyH5L5OJbVDDA4NuQfw92WHiJs9IXzYUFenNbMDUkXpFL+0Tfxtl0Ozm+iThclYU9RO2w29wFuD5w+VifYT1lsg1K5UwPGvONMHXKNyNFhWKBlYUR54L4lQKDiMsMiL2S2t2QiMRgwR0UwgsWOeawPpvptDwTwBrfN/I9m8bGGlOdTg== Received: from DUZPR01CA0006.eurprd01.prod.exchangelabs.com (2603:10a6:10:3c3::13) by DU0PR07MB8737.eurprd07.prod.outlook.com (2603:10a6:10:31b::15) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.21.245.10; Wed, 22 Jul 2026 08:51:52 +0000 Received: from DB5PEPF00014B93.eurprd02.prod.outlook.com (2603:10a6:10:3c3:cafe::83) by DUZPR01CA0006.outlook.office365.com (2603:10a6:10:3c3::13) with Microsoft SMTP Server (version=TLS1_3, cipher=TLS_AES_256_GCM_SHA384) id 15.21.245.10 via Frontend Transport; Wed, 22 Jul 2026 08:51:52 +0000 X-MS-Exchange-Authentication-Results: spf=pass (sender IP is 192.176.1.74) smtp.mailfrom=ericsson.com; dkim=none (message not signed) header.d=none;dmarc=pass action=none header.from=ericsson.com; Received-SPF: Pass (protection.outlook.com: domain of ericsson.com designates 192.176.1.74 as permitted sender) receiver=protection.outlook.com; client-ip=192.176.1.74; helo=oa.msg.ericsson.com; pr=C Received: from oa.msg.ericsson.com (192.176.1.74) by DB5PEPF00014B93.mail.protection.outlook.com (10.167.8.231) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.21.245.5 via Frontend Transport; Wed, 22 Jul 2026 08:51:52 +0000 Received: from seroius18814.sero.gic.ericsson.se (153.88.142.248) by smtp-central.internal.ericsson.com (100.87.178.66) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.2562.37; Wed, 22 Jul 2026 10:51:50 +0200 Received: from seroius08462.sero.gic.ericsson.se (seroius08462.sero.gic.ericsson.se [10.63.237.245]) by seroius18814.sero.gic.ericsson.se (Postfix) with ESMTP id 078084020C02; Wed, 22 Jul 2026 10:51:49 +0200 (CEST) Received: by seroius08462.sero.gic.ericsson.se (Postfix, from userid 160155) id D2F8B700DBB0; Wed, 22 Jul 2026 10:51:49 +0200 (CEST) From: To: CC: Daniel Turull Subject: [PATCH v2 1/4] scripts/contrib: add helper to report and fix missing SRC_URI ;tag= Date: Wed, 22 Jul 2026 10:51:35 +0200 Message-ID: <20260722085140.2355003-2-daniel.turull@ericsson.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260722085140.2355003-1-daniel.turull@ericsson.com> References: <20260722085140.2355003-1-daniel.turull@ericsson.com> MIME-Version: 1.0 X-EOPAttributedMessage: 0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: DB5PEPF00014B93:EE_|DU0PR07MB8737:EE_ X-MS-Office365-Filtering-Correlation-Id: 66e8fce1-41ef-40e2-3275-08dee7ce79b9 X-SMTP-Server: smtp-central.internal.ericsson.com X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|1800799024|36860700016|82310400026|23010399003|376014|22082099003|18002099003|3023799007|6133799003|11063799006|56012099006|10067099003; X-Microsoft-Antispam-Message-Info: 5lVt9rU/tlAvY8gkshTjrLM9CJnwUnzljgpBx9SWsU5LuU8pku9UiB2f5VYv4at13dFpIWF+iWh/3JCea59UAqx9+VSdAYZCTECBpUfKsBJOOLP1wXD4XOW1TqMyBIvLV5gJRfSN8ZkMJ9QCCUeNe6d4bA4HOUoDdyOuBTpYc4ViuQ+6nyQI95/nsir8Biw6MCmyDWFOpMLDil9/ozOhY28usgCzkj5R/5WTx8ZbR8QZ5ALtoC0SYh6lbx5OlYQk80lsgv/cflV4iO9kJXKRfEme0dFmhHj7y9uZEHh/WtebBRlDPcRS+SsVYFQtV2UkjE8cX78m2+x795+rIGYymoBl4cQ1MdX37JxIT/RSG5ifI3aCjfxTDHnPAbWv7ZeY9YFSvZbo1Zdh0yz2+26IHswTTJDJdpGTuuB4rh6AnAcvq94fThND2gOXO2P9r850MpUsdZus5lPvFQlqzT0Wv5MVVAd/QtnG07b3CsLhAaNhKkFX3W2d8acqJ0YbX1UFDQZLrm1TH3RDytxlopu45IgDoOBnmT0LA6gGWBXjkiRrNXeqv+66Y//DO8kqnDhzR29P+pTiP9ZGwugWdttJSKWsg70jS3s2c7YqR/JRijlILPmLoqbAuNKHMCEEmmE8yXdIMgpllRj5371O0IRIAkc+Jg1ldppHyRI6fiIG2O4jdXandrHvCmb5hAciEgu33lQrthqkJQGrGyNLaAi2iQ== X-Forefront-Antispam-Report: CIP:192.176.1.74;CTRY:SE;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:oa.msg.ericsson.com;PTR:office365.se.ericsson.net;CAT:NONE;SFS:(13230040)(1800799024)(36860700016)(82310400026)(23010399003)(376014)(22082099003)(18002099003)(3023799007)(6133799003)(11063799006)(56012099006)(10067099003);DIR:OUT;SFP:1101; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: FhsIpt8NxWljraloalcLZjH75a8Xr9siDQkjixR8nmBBoI+NHbf6J+YPSJ0DtFT97CKxggtfvPMMOf5Ab+qzoJ1yu6FxHor7q+vgG9yB/K+GN3jxB8jkmXrMUat3/AZVwUh/dJngTVTVCQRgjE/FY73Ql8ihFzq3qtyDgHrlwjD/tsBW6FHecmw83HJ9/Bb7VRNlHgtsOQdfuVILG/2GH8t5sj/i37LwC9Tn4nObWkLxQiwUaRi9tDczG+wSDlQzTsXlGa9J6aIFSi9pcWyMncItic6GdLWOw7i9Z3XY1Xj37N1XLDJc1n383mgRf5vww3xHwsTGEDSafnIeCxAYIp071WkBlyMVp58E2rK4PY2GYpv9U/gOb2CI//edM64LRwlGBJkdqIVs9vh/T17SNcjlC40hmzy/L2SHkt1XBowpr5Lo+m18Iy9PvV/F4Blk X-OriginatorOrg: ericsson.com X-MS-Exchange-CrossTenant-OriginalArrivalTime: 22 Jul 2026 08:51:52.3855 (UTC) X-MS-Exchange-CrossTenant-Network-Message-Id: 66e8fce1-41ef-40e2-3275-08dee7ce79b9 X-MS-Exchange-CrossTenant-Id: 92e84ceb-fbfd-47ab-be52-080c6b87953f X-MS-Exchange-CrossTenant-OriginalAttributedTenantConnectingIp: TenantId=92e84ceb-fbfd-47ab-be52-080c6b87953f;Ip=[192.176.1.74];Helo=[oa.msg.ericsson.com] X-MS-Exchange-CrossTenant-AuthSource: DB5PEPF00014B93.eurprd02.prod.outlook.com X-MS-Exchange-CrossTenant-AuthAs: Anonymous X-MS-Exchange-CrossTenant-FromEntityHeader: HybridOnPrem X-MS-Exchange-Transport-CrossTenantHeadersStamped: DU0PR07MB8737 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 22 Jul 2026 08:52:03 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241632 From: Daniel Turull Add a contrib helper which scans parsed recipes for git/gitsm SRC_URI entries missing a ;tag= parameter. By default it reports only and does not construct the BitBake fetcher. With --query-remote it queries upstream tags and, when a well-known tag format resolves to the recipe's SRCREV, suggests it as a ;tag= candidate. With --write (which implies --query-remote) the unambiguous candidate is written back to the recipe. Tag candidate search uses TAG_FORMATS (v${PV}, ${PV}, ${BPN}-${PV}) followed by UPSTREAM_CHECK_GITTAGREGEX if defined in the recipe. A candidate is only accepted when it resolves to the recipe's literal SRCREV on the remote, so floating revisions (AUTOREV) and non-release PVs are skipped. Writes are performed via oe.recipeutils.patch_recipe(), which also localises the change to the .inc file when SRC_URI is defined there. Only literal SRC_URI entries (no ${...} in the URL token) are modified; the entry is edited with bb.fetch2.decodeurl()/encodeurl() so the ;tag= parameter is appended without disturbing the other parameters. Every modified file is printed, with a reminder to review the result with git diff and test it with bitbake -c fetch. With --recipe and --layer the scan can be restricted to a single recipe or layer, and --fail-on-missing makes the exit status reflect URLs still missing after any writes complete. AI-Generated: Kiro with Claude-opus-4.8 Signed-off-by: Daniel Turull --- scripts/contrib/check-srcuri-tag.py | 483 ++++++++++++++++++++++++++++ 1 file changed, 483 insertions(+) create mode 100755 scripts/contrib/check-srcuri-tag.py diff --git a/scripts/contrib/check-srcuri-tag.py b/scripts/contrib/check-srcuri-tag.py new file mode 100755 index 0000000000..33a2b781f7 --- /dev/null +++ b/scripts/contrib/check-srcuri-tag.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: OpenEmbedded Contributors +# +# SPDX-License-Identifier: MIT + +# pylint: disable=invalid-name + +""" +Scan parsed recipes for git/gitsm SRC_URI entries missing a ;tag= parameter. + +Default mode reports only. With --query-remote the script queries upstream +tags and, when a well-known tag format (v${PV}, ${PV}, ${BPN}-${PV} or the +recipe's UPSTREAM_CHECK_GITTAGREGEX) resolves to the recipe's SRCREV, suggests +it as a ;tag= candidate. With --write (which implies --query-remote) the +unambiguous candidate is written directly into the file that literally +defines the SRC_URI entry (which may be a .inc file required by the +recipe). Only literal SRC_URI entries (no ${...} in the URL token) are +modified. + +Must be run from an initialized build environment (oe-init-build-env). + +Usage examples: + check-srcuri-tag.py + check-srcuri-tag.py --query-remote + check-srcuri-tag.py --write + check-srcuri-tag.py --recipe busybox --write + check-srcuri-tag.py --layer /path/to/meta-foo + check-srcuri-tag.py --fail-on-missing +""" + +import argparse +import logging +import os +import re +import sys +from dataclasses import dataclass + +scripts_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +sys.path.insert(0, os.path.join(scripts_path, 'lib')) + +import scriptpath # pylint: disable=import-error,wrong-import-position +if not scriptpath.add_bitbake_lib_path(): + sys.exit("Unable to find bitbake libraries") +scriptpath.add_oe_lib_path() + +import bb.cache # pylint: disable=wrong-import-position +import bb.fetch2 # pylint: disable=wrong-import-position +import bb.tinfoil # pylint: disable=wrong-import-position +import oe.recipeutils # pylint: disable=wrong-import-position + +# Well-known tag formats tried in order when --query-remote is active. +TAG_FORMATS = ["v${PV}", "${PV}", "${BPN}-${PV}"] + +# Cache of remote tags keyed by upstream repository identity. +_lsremote_cache = {} + + +@dataclass(eq=False) +class UrlResult: + """Outcome for a single git URL within a recipe.""" + entry: str # unexpanded SRC_URI token + base_url: str # expanded scheme://host/path (no parameters) + name: str # name= parameter, or "" + status: str # tagged|missing|remote-error|parse-error + tag: str = "" # existing ;tag= value (status == tagged) + tag_fmt: str = "" # suggested tag candidate (status == missing) + detail: str = "" # human-readable note + + +def get_literal_srcrev(name, data): + """Return (sha, None) or (None, reason) for the named git URL. + + SRCREV candidates are read unexpanded (to avoid triggering AUTOREV + resolution) in the same fallback order used by bb.fetch2, accepting only a + plain hex SHA (or the SHA embedded in a cached AUTOINC+ value). + """ + pn = data.getVar("PN") or "" + attempts = [] + if name and name != "default": + if pn: + attempts.append(f"SRCREV_{name}:pn-{pn}") + attempts.append(f"SRCREV_{name}") + if pn: + attempts.append(f"SRCREV:pn-{pn}") + attempts.append("SRCREV") + + for key in attempts: + val = data.getVar(key, False) + if not val or val == "INVALID": + continue + if val.startswith("AUTOINC+"): + val = val[len("AUTOINC+"):] + if "AUTOREV" in val or "AUTOINC" in val: + return None, "SRCREV is a floating revision" + if "${" in val: + return None, "SRCREV is not a literal value" + val = val.lower() + if re.fullmatch(r'[0-9a-f]{40}|[0-9a-f]{64}', val): + return val, None + return None, "SRCREV is not a literal SHA" + return None, "SRCREV is not set" + + +def get_release_pv(data): + """Return (pv, None) for a plain release PV, or (None, reason). + + The unexpanded PV is checked first so PV = "${SRCPV}" or "1.0+git" are + rejected before any expansion side effects occur. + """ + pv_raw = data.getVar("PV", False) or "" + if not pv_raw or "git" in pv_raw or "SRCPV" in pv_raw: + return None, f"PV '{pv_raw}' is not a plain release version" + return data.getVar("PV") or pv_raw, None + + +def lsremote_tags(ud, data): + """Return ({tag_name: frozenset(shas)}, None) or (None, reason). + + Uses the Git fetcher's _lsremote() so mirrors, credentials and + BB_NO_NETWORK are honoured. Both the tag-object and peeled-commit SHAs of + annotated tags are collected under the same tag name. + """ + key = (getattr(ud, 'user', ''), getattr(ud, 'proto', ''), + getattr(ud, 'host', ''), getattr(ud, 'path', '')) + if key in _lsremote_cache: + return _lsremote_cache[key] + + try: + output = ud.method._lsremote(ud, data, "refs/tags/*") + except (bb.fetch2.NetworkAccess, bb.fetch2.FetchError) as exc: + result = (None, f"remote error: {exc}") + _lsremote_cache[key] = result + return result + + tag_map = {} + for line in output.splitlines(): + parts = line.split(None, 1) + if len(parts) != 2: + continue + sha, ref = parts + tag = ref.removeprefix("refs/tags/").removesuffix("^{}") + tag_map.setdefault(tag, set()).add(sha) + + result = ({k: frozenset(v) for k, v in tag_map.items()}, None) + _lsremote_cache[key] = result + return result + + +def deduce_tag_format(ud, srcrev, pv, bpn, data): + """Return (tag_fmt, None) if a well-known format resolves to srcrev on the + remote, else (None, reason). + + Tries TAG_FORMATS first; if none match and the recipe defines + UPSTREAM_CHECK_GITTAGREGEX, that regex is used to find the tag whose + captured 'pver' group (normalized from '_' to '.') equals PV and whose SHA + matches SRCREV. A unique regex hit is returned as a literal tag name. + """ + tag_map, err = lsremote_tags(ud, data) + if tag_map is None: + return None, err + if not tag_map: + return None, "remote has no tags" + + matches = [fmt for fmt in TAG_FORMATS + if srcrev in tag_map.get( + fmt.replace("${PV}", pv).replace("${BPN}", bpn), frozenset())] + if len(matches) == 1: + return matches[0], None + if len(matches) > 1: + return None, f"ambiguous: multiple formats match ({', '.join(matches)})" + + check_regex = data.getVar("UPSTREAM_CHECK_GITTAGREGEX", False) + if check_regex: + try: + compiled = re.compile(check_regex) + except re.error as exc: + return None, f"UPSTREAM_CHECK_GITTAGREGEX is invalid: {exc}" + regex_matches = [] + for tag_name, shas in tag_map.items(): + m = compiled.fullmatch(tag_name) + if (m and "pver" in m.groupdict() + and m.group("pver").replace("_", ".") == pv + and srcrev in shas): + regex_matches.append(tag_name) + if len(regex_matches) == 1: + return regex_matches[0], None + if len(regex_matches) > 1: + return None, ("ambiguous: UPSTREAM_CHECK_GITTAGREGEX matched " + f"multiple tags ({', '.join(sorted(regex_matches))})") + + return None, "no well-known tag format matched SRCREV on remote" + + +def add_candidate(result, exp_url, name, data): + """Fill in result.tag_fmt/detail for a missing-tag URL under --query-remote. + + Sets result.status to 'remote-error' if the remote could not be queried. + """ + srcrev, reason = get_literal_srcrev(name or "default", data) + if not srcrev: + result.detail = f"no suggestion: {reason}" + return + pv, reason = get_release_pv(data) + if not pv: + result.detail = f"no suggestion: {reason}" + return + + try: + ud = bb.fetch2.FetchData(exp_url, data) + except bb.fetch2.FetchError as exc: + result.status = "remote-error" + result.detail = str(exc) + return + + fmt, reason = deduce_tag_format(ud, srcrev, pv, data.getVar("BPN") or "", data) + if fmt: + result.tag_fmt = fmt + result.detail = f"candidate: ;tag={fmt}" + elif reason.startswith("remote error"): + result.status = "remote-error" + result.detail = reason + else: + result.detail = f"no suggestion: {reason}" + + +def check_url(entry, data, args): + """Process a single git/gitsm SRC_URI entry. Returns a UrlResult.""" + exp = data.expand(entry) + try: + scheme, host, path, _u, _p, parm = bb.fetch2.decodeurl(exp) + except bb.fetch2.MalformedUrl as exc: + return UrlResult(entry, exp, "", "parse-error", detail=str(exc)) + + base_url = f"{scheme}://{host}{path}" + name = parm.get("name", "") + # The git fetcher no longer supports comma-separated name= values. + if "," in name: + return UrlResult(entry, base_url, name, "parse-error", + detail="comma in name= parameter") + if parm.get("tag"): + return UrlResult(entry, base_url, name, "tagged", tag=parm["tag"]) + + result = UrlResult(entry, base_url, name, "missing") + if args.query_remote: + add_candidate(result, exp, name, data) + return result + + +def check_recipe(data, args): + """Return a list of UrlResult for every git/gitsm SRC_URI entry.""" + entries = oe.recipeutils.split_var_value( + data.getVar("SRC_URI", False) or "", assignment=False) + return [check_url(e, data, args) for e in entries + if e.startswith(("git://", "gitsm://"))] + + +def add_tag_to_entry(entry, tag_fmt): + """Return entry with ;tag= added (entry must be a literal URL).""" + decoded = list(bb.fetch2.decodeurl(entry)) + decoded[5]["tag"] = tag_fmt + return bb.fetch2.encodeurl(decoded) + + +def find_source_file(entry, data): + """Return (filepath, None) for the one file whose SRC_URI fragment + literally contains entry, or (None, reason) if that can't be determined + unambiguously. + + entry must contain no variable references (checked by the caller). Only + files recorded in SRC_URI's variable history are considered, and entry + must appear as a whole space-separated token in exactly one of them (this + also catches the case where the git URL is set in a .inc file that is + require'd by the .bb, which is common for git recipes). + """ + matches = [] + for event in data.varhistory.variable("SRC_URI") or []: + fn = event.get("file") + detail = event.get("detail") + if not fn or not detail: + continue + if entry in detail.split() and fn not in matches: + matches.append(fn) + + if not matches: + return None, "URL not found literally in SRC_URI history" + if len(matches) > 1: + return None, f"URL set in multiple files: {', '.join(matches)}" + return matches[0], None + + +def apply_tag(fn, entry, tag_fmt): + """Replace entry with entry;tag= in fn. entry must appear as a + whole space/quote-delimited token exactly once. + + Returns (True, None) on success, or (False, reason) on error. + """ + try: + with open(fn, encoding="utf-8") as f: + content = f.read() + except OSError as exc: + return False, f"read error: {exc}" + + pattern = re.compile(r'(?") + + if args.fail_on_missing and stats["missing"] - stats["written"] > 0: + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main())