Skip to content

Commit c68003d

Browse files
committed
frontend, backend, rpmbuild: extract Exclu*Arch/BuildArch for all targets
Relates: #1315 Relates: #4088
1 parent 09f3137 commit c68003d

File tree

8 files changed

+226
-18
lines changed

8 files changed

+226
-18
lines changed

backend/copr_backend/background_worker_build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
MAX_HOST_ATTEMPTS = 3
4646
MAX_SSH_ATTEMPTS = 5
47-
MIN_BUILDER_VERSION = "1.3.1"
47+
MIN_BUILDER_VERSION = "1.6.1"
4848
CANCEL_CHECK_PERIOD = 5
4949
DATETIME_FORMAT = "%Y-%m-%d %H:%M"
5050

backend/copr_backend/frontend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from copr_backend.exceptions import FrontendClientException
1010

1111
# The frontend counterpart is in `backend_general:send_frontend_version`
12-
MIN_FE_BE_API = 7
12+
MIN_FE_BE_API = 8
1313

1414
class FrontendClient:
1515
"""

frontend/coprs_frontend/coprs/logic/builds_logic.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,16 +1144,16 @@ def finish(chroot, status):
11441144
# Skip excluded architectures
11451145
if upd_dict.get("chroot") == "srpm-builds" and upd_dict.get("results"):
11461146
for chroot in build.build_chroots:
1147+
tags = upd_dict["results"]["architecture_specific_tags"]
1148+
tags = tags[chroot.mock_chroot.name_release]
11471149
arch = chroot.mock_chroot.arch
1148-
1149-
exclusivearch = upd_dict["results"].get("exclusivearch")
1150+
exclusivearch = tags.get("exclusivearch")
11501151
if exclusivearch and arch not in exclusivearch:
11511152
chroot.status_reason = \
11521153
"This chroot was skipped because of ExclusiveArch"
11531154
finish(chroot, StatusEnum("skipped"))
1154-
1155-
excludearch = upd_dict["results"].get("excludearch")
1156-
if arch in excludearch:
1155+
excludearch = tags.get("excludearch")
1156+
if excludearch and arch in excludearch:
11571157
chroot.status_reason = \
11581158
"This chroot was skipped because of ExcludeArch"
11591159
finish(chroot, StatusEnum("skipped"))

frontend/coprs_frontend/coprs/views/backend_ns/backend_general.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def send_frontend_version(response):
2525
setup the version according to our needs.
2626
For the backend counterpart, see the `MIN_FE_BE_API` constant.
2727
"""
28-
response.headers['Copr-FE-BE-API-Version'] = '7'
28+
response.headers['Copr-FE-BE-API-Version'] = '8'
2929
return response
3030

3131

@@ -241,6 +241,10 @@ def get_srpm_build_record(task, for_backend=False):
241241
"package_name": task.package.name if task.package else None,
242242
"appstream": bool(task.copr.appstream),
243243
"repos": BuildConfigLogic.get_additional_repo_views(repos, chroot),
244+
"distributions_in_project": sorted(list({x.name_release for x in
245+
task.copr.active_chroots})),
246+
"distributions_in_build": sorted(list({x.mock_chroot.name_release for x
247+
in task.build_chroots})),
244248
})
245249

246250
return build_record

frontend/coprs_frontend/tests/test_logic/test_builds_logic.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,9 +680,12 @@ def test_skipping_chroots(self, exclusivearch, states):
680680
"pkg_version": 1,
681681
"chroot": "srpm-builds",
682682
"results": {
683+
"architecture_specific_tags": {
684+
"fedora-17": {
685+
"exclusivearch": exclusivearch,
686+
}
687+
},
683688
"epoch": None,
684-
"excludearch": [],
685-
"exclusivearch": exclusivearch,
686689
"name": "biosdevname",
687690
"release": "17",
688691
"version": "0.7.3"

rpmbuild/copr-rpmbuild.spec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Requires: %1 \
1414
%{expand: %%global latest_requires_packages %1 %%{?latest_requires_packages}}
1515

1616
Name: copr-rpmbuild
17-
Version: 1.6
17+
Version: 1.6.1
1818
Summary: Run COPR build tasks
1919
Release: 1%{?dist}
2020
URL: https://github.com/fedora-copr/copr
@@ -35,6 +35,7 @@ BuildRequires: %{python}-daemon
3535
BuildRequires: %{python}-devel
3636
BuildRequires: %{python}-distro
3737
BuildRequires: %{python}-httmock
38+
BuildRequires: python3-norpm
3839
BuildRequires: %{rpm_python}
3940
BuildRequires: asciidoc
4041
BuildRequires: dist-git-client
@@ -70,6 +71,7 @@ Requires: %{python_pfx}-specfile >= 0.21.0
7071
Requires: python3-backoff >= 1.9.0
7172
Requires: python3-daemon
7273
Requires: python3-pyyaml
74+
Requires: python3-norpm
7375

7476
Requires: mock >= 5.0
7577
Requires(pre): mock-filesystem

rpmbuild/copr_rpmbuild/automation/srpm_results.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77

88
from copr_rpmbuild.automation.base import AutomationTool
9+
from copr_rpmbuild.extract_specfile_tags import get_architecture_specific_tags
910
from copr_rpmbuild.helpers import (
1011
get_rpm_header,
1112
macros_for_task,
@@ -38,25 +39,73 @@ def run(self):
3839
with open(path, "w", encoding="utf-8") as dst:
3940
dst.write(data_json)
4041

42+
@property
43+
def target_distros(self):
44+
"""
45+
Get the list of distributions we build this package against.
46+
"""
47+
# Handle distributions_in_build first to optimize a bit; the
48+
# distributions_in_build is a subset of distributions_in_project
49+
# and if available - we can avoid some macro expansion cycles.
50+
for field_name in ["distributions_in_build",
51+
"distributions_in_project"]:
52+
if self.task[field_name]:
53+
self.log.info("Using %s for this build.", field_name)
54+
return self.task[field_name]
55+
raise RuntimeError("Running against too old copr-frontend")
56+
4157
def get_package_info(self):
4258
"""
4359
Return ``dict`` with interesting package metadata
4460
"""
45-
keys = ["name", "epoch", "version", "release",
46-
"exclusivearch", "excludearch"]
61+
output_tags = {}
62+
63+
# While this is highly inconvenient, many packages still use %lua to
64+
# define these fields, and Copr must be able to build them. Unlike
65+
# other "rpm parsing" use-cases, this does not pose a security risk; we
66+
# run this script on an disposable worker, so there are no serious
67+
# consequences if a user "bricks" the machine.
68+
#
69+
# Although these fields may expand into target-specific values in
70+
# theory, we need a single NEVRA for single build. Consequently,
71+
# we do not perform separate expansions for each individual target
72+
# distribution. Note these fields are not critical for the overall
73+
# build process, we store some metadata about the build sing these.
74+
#
75+
# To fully resolve the issue #1315, we have to fix the
76+
# backend → distgit protocol, and upload the right srpm. Or even
77+
# better, upload all possible src.rpm variants.
78+
rpm_tags = ["name", "epoch", "version", "release"]
79+
80+
# These are a bit more important, since these are used to decide which
81+
# BuildChroots are going to be skipped or not. And we need to extract
82+
# them for each target distribution version separately.
83+
norpm_tags = ["exclusivearch", "excludearch", "buildarch"]
84+
85+
specfile_path = locate_spec(self.resultdir)
86+
87+
# TODO: host override_database= on backend and configure
88+
output_tags["architecture_specific_tags"] = get_architecture_specific_tags(
89+
specfile_path,
90+
norpm_tags,
91+
self.target_distros,
92+
log=self.log,
93+
)
94+
4795
try:
4896
macros = macros_for_task(self.task, self.config)
49-
path = locate_spec(self.resultdir)
50-
spec = Spec(path, macros)
51-
return {key: getattr(spec, key) for key in keys}
97+
spec = Spec(specfile_path, macros)
98+
output_tags.update({key: getattr(spec, key) for key in rpm_tags})
5299

53100
except Exception: # pylint: disable=broad-exception-caught
54101
# Specfile library raises too many exception to name the
55102
# in except block
56-
msg = "Exception appeared during handling spec file: {0}".format(path)
103+
msg = "Exception appeared during handling spec file: {0}".format(specfile_path)
57104
self.log.exception(msg)
58105

59106
path = locate_srpm(self.resultdir)
60107
self.log.warning("Querying NEVRA from SRPM header: %s", path)
61108
hdr = get_rpm_header(path)
62-
return {key: hdr[key] for key in keys}
109+
output_tags.update({key: hdr[key] for key in rpm_tags})
110+
111+
return output_tags
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
Extract macro-expanded tag value (e.g., BuildArch) from given specfile.
3+
"""
4+
5+
import logging
6+
import os
7+
import tempfile
8+
9+
from copr_common.request import SafeRequest
10+
11+
from norpm.macrofile import system_macro_registry
12+
from norpm.specfile import specfile_expand, ParserHooks
13+
from norpm.overrides import override_macro_registry
14+
from norpm.exceptions import NorpmError
15+
16+
DEFAULT_OVERRIDE_URL = "https://raw.githubusercontent.com/praiskup/norpm-macro-overrides/refs/heads/main/distro-arch-specific.json"
17+
18+
DEFAULT_TAG_MAP = {
19+
# TODO: extract rhel+epel into the datafile
20+
"epel-7": "rhel-7",
21+
"epel-8": "rhel-8",
22+
"epel-9": "rhel-9",
23+
"epel-10": "centos-stream+epel-10",
24+
# what do we do about custom-* chroots?
25+
"custom-0": "fedora-rawhide",
26+
"custom-1": "fedora-rawhide",
27+
}
28+
29+
class _TagHooks(ParserHooks):
30+
""" Gather access to spec tags """
31+
def __init__(self, expanded_tags):
32+
self.expanded_tags = set(expanded_tags)
33+
self.tags = {}
34+
35+
def tag_found(self, name, value, _tag_raw):
36+
"""
37+
Parser hook that gathers the tags' values, if defined.
38+
"""
39+
if name not in self.expanded_tags:
40+
return
41+
if name not in self.tags:
42+
self.tags[name] = []
43+
# tags may be specified multiple times within a single spec file
44+
self.tags[name] += [value]
45+
46+
47+
def collapse_tag_values_cb(array_of_tag_values):
48+
"""
49+
Process tags that represent sets of strings (e.g., ExcludeArch).
50+
51+
If a tag is specified multiple times within the specfile, this function
52+
performs a union of all encountered values. The resulting set contains
53+
every unique string defined across all instances of the tag.
54+
55+
Returns:
56+
set: A set of all unique strings extracted from the tag definitions.
57+
"""
58+
concat = " ".join(array_of_tag_values)
59+
return list(set(concat.split()))
60+
61+
62+
def extract_tags_from_specfile(specfile, extract_tags, override_database=None,
63+
target=None, tag_cb=None):
64+
"""
65+
Parse the given SPECFILE against system macros and optionally TARGET macros.
66+
67+
If TARGET is specified, OVERRIDE_DATABASE (a file path) must also be
68+
provided. If TARGET is omitted, the local system macros are used by
69+
default.
70+
71+
Args:
72+
specfile (str): Path to the specfile to be parsed.
73+
target (str, optional): Target distribution (e.g., "rhel-7").
74+
override_database (str, optional): Database file path required if target
75+
is set.
76+
tag_cb (callable, optional): A callback function used to transform the
77+
list of values for each tag. If provided, each item in the return
78+
dictionary is passed through this function. A common choice is
79+
`collapse_tag_values_cb`, which collapses the list into a set of
80+
unique strings.
81+
82+
Returns:
83+
dict: A mapping of lowercase tagnames to their processed values.
84+
Default format: {'excludearch': ['ppc64 ppc64le', 's390x i386']}
85+
With tag_cb: The value type is determined by the callback's return.
86+
With tag_cb=collapse_tag_values_cb:
87+
{'excludearch': {'ppc64', 'ppc64le', 's390x', 'i386'}}
88+
Note:
89+
Since RPM tags are case-insensitive, all tagnames are normalized
90+
using .lower().
91+
"""
92+
registry = system_macro_registry()
93+
if override_database:
94+
registry = override_macro_registry(registry, override_database, target)
95+
96+
# %dist definition contains %lua mess, it's safer to clear it (since we
97+
# don't necessarily need it)
98+
registry["dist"] = ""
99+
100+
# norpm maintains a few tricks to ease the spec file parsing
101+
registry.known_norpm_hacks()
102+
103+
tags = _TagHooks(extract_tags)
104+
try:
105+
with open(specfile, "r", encoding="utf8") as fd:
106+
specfile_expand(fd.read(), registry, tags)
107+
except NorpmError as err:
108+
print("WARNING: Building for all architectures since "
109+
f"the spec file parser: failed: {err}")
110+
111+
if not tag_cb:
112+
return tags.tags
113+
114+
processed = {}
115+
for tag, values in tags.tags.items():
116+
processed[tag] = tag_cb(values)
117+
118+
return processed
119+
120+
121+
def get_architecture_specific_tags(specfile, extract_tags, targets,
122+
override_database=DEFAULT_OVERRIDE_URL,
123+
log=logging):
124+
"""
125+
A high-level tool for Copr, working with temporary files, etc.
126+
"""
127+
architecture_tags = {}
128+
request = SafeRequest(log=log)
129+
response = request.get(override_database)
130+
with tempfile.NamedTemporaryFile(mode='wb', delete=False) as temp:
131+
temp_path = temp.name
132+
temp.write(response.content)
133+
134+
try:
135+
for distro in targets:
136+
ask = DEFAULT_TAG_MAP.get(distro, distro)
137+
log.info("Extracting arch-specific tags for %s", distro)
138+
architecture_tags[distro] = extract_tags_from_specfile(
139+
specfile,
140+
extract_tags,
141+
override_database=temp_path,
142+
target=ask,
143+
tag_cb=collapse_tag_values_cb
144+
)
145+
finally:
146+
try:
147+
os.unlink(temp_path)
148+
except OSError:
149+
pass
150+
return architecture_tags

0 commit comments

Comments
 (0)