specfile
Python library for parsing and manipulating RPM spec files. Main focus is on modifying existing spec files, any change should result in a minimal diff.
Motivation
Originally, rebase-helper provided an API for spec file modifications that was also used by packit. The goal of this project is to make the interface more general and convenient to use by not only packit but also by other Python projects that need to interact with RPM spec files.
Important terms used in this library
Section
Section is a spec file section, it has a well-defined name that starts with % character and that can optionally be followed by arguments.
In this library, the starting % of section name is omitted for convenience.
There is a special section internally called %package
, often also referred to as preamble, and it represents the content of the spec file that precedes the first named section (usually %description
). This section contains the main package metadata (tags). Metadata of subpackages are defined in subsequent %package
sections, that are not anonymous and are always followed by arguments specifying the name of the subpackage (e.g. %package doc
or %package -n completely-different-subpackage-name
).
Tag
Tag represents a single item of metadata of a package. It has a well-defined name and a value. Tags are defined in %package
sections.
For the purposes of this library, a tag can have associated comments. These are consecutive comment lines directly above the tag definition in a spec file.
Source
Source is a source file or a downstream patch defined by a Source
/Patch
tag or by an entry in %sourcelist
/%patchlist
section.
Source can be local, specified by a filename, or remote, specified by a URL. Local sources should be located in a directory referred to as sourcedir
. Remote sources should be downloaded to this directory.
Sources defined by tags can be explicitly numbered, e.g. Source0
or Patch999
, otherwise implicit numbering takes place and source numbers are auto-assigned in a sequential manner.
Prep macros
Prep macros are macros that often appear in (and only in, they don't make sense anywhere else) %prep
section.
4 such macros are recognized by this library, %setup
, %patch
, %autosetup
and %autopatch
. A typical spec file uses either %autosetup
or a combination of %setup
and %patch
or %autopatch
.
Documentation
Full documentation generated from code.
Examples and use cases
The following examples should cover use cases required by packit.
Instantiating
from specfile import Specfile
specfile = Specfile('/tmp/test.spec')
specfile = Specfile('test.spec', sourcedir='/tmp/sources')
Reloading
specfile.reload()
Saving changes
specfile = Specfile('test.spec')
...
specfile.save()
specfile = Specfile('test.spec', autosave=True)
with Specfile('test.spec') as specfile:
...
Defining and undefining macros
specfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])
specfile = Specfile('test.spec', macros=[('rhel', None)])
Low-level manipulation
with specfile.sections() as sections:
sections.prep = ['%autosetup -p1']
del sections.changelog
sections[1], sections[2] = sections[2], sections[1]
print(sections.get('package devel'))
sections.build.insert(0, 'export VERBOSE=1')
with specfile1.sections() as sections1, with specfile2.sections() as sections2:
sections2.changelog[:] = sections1.changelog
Mid-level manipulation - tags, changelog and prep
with specfile.tags() as tags:
print(tags[0].name)
print(tags[0].value)
print(tags[0].expanded_value)
print(tags[0].comments)
print(tags.url)
tags.url = 'https://example.com'
with specfile.tags('package devel') as tags:
print(tags.requires)
with specfile.changelog() as changelog:
print(changelog[-1])
changelog[1].content.append('- another line')
del changelog[0]
from specfile.prep import AutosetupMacro
with specfile.prep() as prep:
print(prep.macros[0].name)
print('%autosetup' in prep)
print(AutosetupMacro in prep)
prep.autosetup.options.n = '%{srcname}-%{version}'
prep.add_patch_macro(28, p=1, b='.test')
del prep.patch0
prep.remove_patch_macro(0)
High-level manipulation
Version and release
print(specfile.version)
print(specfile.release)
specfile.version = '2.1'
specfile.release = '3'
specfile.set_version_and_release('2.1', release='3')
specfile.set_version_and_release('2.1', preserve_macros=True)
Bumping release
To bump release and add a new changelog entry, you could use the following code:
from specfile import Specfile
with Specfile("example.spec") as spec:
spec.release = str(int(spec.expanded_release) + 1)
spec.add_changelog_entry("- Bumped release for test purposes")
Changelog
specfile.add_changelog_entry('- New upstream release 2.1')
specfile.add_changelog_entry(
'- New upstream release 2.1',
author='Nikola Forró',
email='nforro@redhat.com',
timestamp=datetime.date(2021, 11, 20),
)
if specfile.has_autochangelog:
Sources and patches
with specfile.sources() as sources:
print(sources[0].expanded_location)
sources.append('tests.tar.gz')
with specfile.patches() as patches:
patches[0].location = 'downstream.patch'
patches[-1].comments.clear()
patches.append('another.patch')
del patches[2]
patches.insert_numbered(999, 'final.patch')
specfile.add_patch('necessary.patch', comment='a human-friendly comment to the patch')
Other attributes
print(specfile.name)
print(specfile.license)
print(specfile.summary)
specfile.url = 'https://example.com'
Note that if you want to access multiple tag values, it may be noticeably faster to do it using the tags
context manager:
with specfile.tags() as tags:
print(tags.name.value)
print(tags.license.value)
print(tags.summary.value)
tags.url.value = 'https://example.com'
Read-only access
If you don't need write access, you can use the content
property of context managers and avoid the with
statement:
tags = specfile.tags().content
print(tags.version.expanded_value)
print(tags.release.expanded_value)
print(len(specfile.sources().content))
Validity
Macro definitions, tags, %sourcelist
/%patchlist
entries and sources/patches have a valid
attribute. An entity is considered valid if it isn't present in a false branch of any condition.
Consider the following in a spec file:
%if 0%{?fedora} >= 36
Recommends: %{name}-selinux
%endif
Provided there are no other Recommends
tags, the following would print True
or False
depending on the value of the %fedora
macro:
with specfile.tags() as tags:
print(tags.recommends.valid)
You can define macros or redefine/undefine system macros using the macros
argument of the constructor or by modifying the macros
attribute of a Specfile
instance.
The same applies to %ifarch
/%ifos
statements:
%ifarch %{java_arches}
BuildRequires: java-devel
%endif
Provided there are no other BuildRequires
tags, the following would print True
in case the current platform was part of %java_arches
:
with specfile.tags() as tags:
print(tags.buildrequires.valid)
To override this, you would have to redefine the %_target_cpu
system macro (or %_target_os
in case of %ifos
).
Videos
Here is a demo showcasing the Specfile.update_tag()
method and its use cases: