Skip to main content

A library for parsing and manipulating RPM spec files.

Project description

specfile

CI Documentation License

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

# using an absolute path
specfile = Specfile('/tmp/test.spec')

# using a relative path and a different sourcedir
specfile = Specfile('test.spec', sourcedir='/tmp/sources')

Reloading

# if the spec file happens to be modified externally, it can be reloaded
specfile.reload()

Saving changes

# no autosave
specfile = Specfile('test.spec')
...
# saving explicitly when needed
specfile.save()

# enabling autosave, changes are saved immediately after any modification
specfile = Specfile('test.spec', autosave=True)

# as a context manager, saving is performed at context exit
with Specfile('test.spec') as specfile:
    ...

Defining and undefining macros

# override macros loaded from system macro files
specfile = Specfile('test.spec', macros=[('fedora', '38'), ('dist', '.fc38')])

# undefine a system macro (in case it's defined)
specfile = Specfile('test.spec', macros=[('rhel', None)])

Low-level manipulation

with specfile.sections() as sections:
    # replacing the content of a section
    sections.prep = ['%autosetup -p1']
    # removing a section
    del sections.changelog
    # swapping two sections
    sections[1], sections[2] = sections[2], sections[1]
    # accessing a section with arguments
    print(sections.get('package devel'))
    # inserting a line into a section
    sections.build.insert(0, 'export VERBOSE=1')

# copying a section from one specfile to another
with specfile1.sections() as sections1, with specfile2.sections() as sections2:
    sections2.changelog[:] = sections1.changelog

Mid-level manipulation - tags, changelog and prep

# accessing tags in preamble
with specfile.tags() as tags:
    # name of the first tag
    print(tags[0].name)
    # raw value of the first tag
    print(tags[0].value)
    # expanded value of the first tag
    print(tags[0].expanded_value)
    # comments associated with the first tag
    print(tags[0].comments)
    # value of a tag by name
    print(tags.url)
    tags.url = 'https://example.com'

# accessing tags in subpackages
with specfile.tags('package devel') as tags:
    print(tags.requires)

# working with changelog
with specfile.changelog() as changelog:
    # most recent changelog entry
    print(changelog[-1])
    # making changes
    changelog[1].content.append('- another line')
    # removing the oldest entry
    del changelog[0]

# working with macros in %prep section, supports %setup, %patch, %autosetup and %autopatch
from specfile.prep import AutosetupMacro

with specfile.prep() as prep:
    # name of the first macro
    print(prep.macros[0].name)
    # checking if %autosetup is being used
    print('%autosetup' in prep)
    print(AutosetupMacro in prep)
    # changing macro options
    prep.autosetup.options.n = '%{srcname}-%{version}'
    # adding a new %patch macro
    prep.add_patch_macro(28, p=1, b='.test')
    # removing an existing %patch macro by name
    del prep.patch0
    # this works for both '%patch0' and '%patch -P0'
    prep.remove_patch_macro(0)

High-level manipulation

Version and release

# getting version and release
print(specfile.version)
print(specfile.release)

# setting version and release
specfile.version = '2.1'
specfile.release = '3'

# setting both at the same time (release defaults to 1)
specfile.set_version_and_release('2.1', release='3')

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

# adding a new entry, author is automatically determined
# (using the same heuristics that rpmdev-packager uses) if possible
# this function already honors autochangelog
specfile.add_changelog_entry('- New upstream release 2.1')

# adding a new entry, specifying author and timestamp explicitly
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:
    # do something

Sources and patches

with specfile.sources() as sources:
    # expanded location of the first source
    print(sources[0].expanded_location)
    # adding a source
    sources.append('tests.tar.gz')

with specfile.patches() as patches:
    # modifying location of the first patch
    patches[0].location = 'downstream.patch'
    # removing comments associated with the last patch
    patches[-1].comments.clear()
    # adding and removing patches
    patches.append('another.patch')
    del patches[2]
    # inserting a patch with a specific number
    patches.insert_numbered(999, 'final.patch')

# adding a single 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:

# same as above, but roughly 4x times faster (parsing/saving happens only once)
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:

# no changes done to the tags object will be saved
tags = specfile.tags().content

print(tags.version.expanded_value)
print(tags.release.expanded_value)

# number of sources
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:

Demo of Specfile.update_tag() functionality

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

specfile-0.38.0.tar.gz (114.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

specfile-0.38.0-py3-none-any.whl (67.9 kB view details)

Uploaded Python 3

File details

Details for the file specfile-0.38.0.tar.gz.

File metadata

  • Download URL: specfile-0.38.0.tar.gz
  • Upload date:
  • Size: 114.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for specfile-0.38.0.tar.gz
Algorithm Hash digest
SHA256 fc2b69154157e86a9648ceb80d41f3b944c6eeaf0dff7426a315f7086e977ad8
MD5 a4ed8def6ee6f97b3ff0102f7616b2a8
BLAKE2b-256 86984c1f7b32e4c44b3de74bfeab1d6bd59c0d26e3e500b667c72c22d0e37a0c

See more details on using hashes here.

Provenance

The following attestation bundles were made for specfile-0.38.0.tar.gz:

Publisher: pypi-publish.yml on packit/specfile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file specfile-0.38.0-py3-none-any.whl.

File metadata

  • Download URL: specfile-0.38.0-py3-none-any.whl
  • Upload date:
  • Size: 67.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for specfile-0.38.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec98c3e63cc7ff71bfc3b19a1cfff6d805b3f13687960ed452daf0b7242f92f3
MD5 0f918afa7d307168e91796c4f9751720
BLAKE2b-256 c9b43082935cf47dbf3cccf68e9a779ee5fd50cce84a0ba862904496a7c31367

See more details on using hashes here.

Provenance

The following attestation bundles were made for specfile-0.38.0-py3-none-any.whl:

Publisher: pypi-publish.yml on packit/specfile

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page