Skip to content

Commit 55faf27

Browse files
committed
frontend, backend, rpmbuild: extract Exclu*Arch/BuildArch for all targets
Relates: #1315 Relates: #4088
1 parent 52338dc commit 55faf27

File tree

6 files changed

+206
-11
lines changed

6 files changed

+206
-11
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/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

rpmbuild/copr-rpmbuild.spec

Lines changed: 2 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: %{python}-norpm
3839
BuildRequires: %{rpm_python}
3940
BuildRequires: asciidoc
4041
BuildRequires: dist-git-client

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

0 commit comments

Comments
 (0)