Skip to content

Commit

Permalink
Implement declarative buildsystem support
Browse files Browse the repository at this point in the history
Remove endless boilerplate to perform the same steps in each
package by just declaring which kind of buildsystem the package uses
using the new BuildSystem: spec tag.

Buildsystem macros are just regular parametric macros which can be invoked
manually from specs too, which should help with reuse and handling
complicated cases.

As such, this will only work for simple and/or "perfect" upstream projects
of course. In reality many/most packages need further tweaking, but this
can now be easily done with the new append/prepend modes of the build
scriptlets, and declaratively passed options to specific sections.

Add automake upstream amhello-1.0.tar.gz as an test-case of a rather
hands-free build with added distro specific doc.

Fixes: rpm-software-management#1087
  • Loading branch information
pmatilai authored and dmnks committed Jan 23, 2024
1 parent 70f9c15 commit f76123d
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 0 deletions.
28 changes: 28 additions & 0 deletions build/parsePreamble.c
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,20 @@ int addLangTag(rpmSpec spec, Header h, rpmTagVal tag,
return 0;
}

static int addBuildOption(rpmSpec spec, const char *sect, const char *opt)
{
rpmRC rc = RPMRC_FAIL;
if (*sect == '\0')
sect = "conf";

int sn = getSection(sect);
if (sn >= 0) {
argvAdd(&(spec->buildopts[sn]), opt);
rc = RPMRC_OK;
}
return rc;
}

static rpmRC handlePreambleTag(rpmSpec spec, Package pkg, rpmTagVal tag,
const char *macro, const char *lang)
{
Expand Down Expand Up @@ -820,6 +834,18 @@ static rpmRC handlePreambleTag(rpmSpec spec, Package pkg, rpmTagVal tag,
multiToken = 1;

switch (tag) {
case RPMTAG_BUILDSYSTEM:
SINGLE_TOKEN_ONLY;
if (rpmCharCheck(spec, field,
ALLOWED_CHARS_NAME, ALLOWED_FIRSTCHARS_NAME))
{
goto exit;
}
break;
case RPMTAG_BUILDOPTION:
if (addBuildOption(spec, lang, field))
goto exit;
break;
case RPMTAG_NAME:
SINGLE_TOKEN_ONLY;
if (rpmCharCheck(spec, field,
Expand Down Expand Up @@ -1077,8 +1103,10 @@ static struct PreambleRec_s const preambleList[] = {
{RPMTAG_BUILDARCHS, 0, 0, 0, LEN_AND_STR("buildarchitectures")},
{RPMTAG_BUILDARCHS, 0, 0, 0, LEN_AND_STR("buildarch")},
{RPMTAG_BUILDCONFLICTS, 0, 0, 0, LEN_AND_STR("buildconflicts")},
{RPMTAG_BUILDOPTION, 2, 0, 0, LEN_AND_STR("buildoption")},
{RPMTAG_BUILDPREREQ, 0, 1, 0, LEN_AND_STR("buildprereq")},
{RPMTAG_BUILDREQUIRES, 0, 0, 0, LEN_AND_STR("buildrequires")},
{RPMTAG_BUILDSYSTEM, 0, 0, 1, LEN_AND_STR("buildsystem")},
{RPMTAG_AUTOREQPROV, 0, 0, 0, LEN_AND_STR("autoreqprov")},
{RPMTAG_AUTOREQ, 0, 0, 0, LEN_AND_STR("autoreq")},
{RPMTAG_AUTOPROV, 0, 0, 0, LEN_AND_STR("autoprov")},
Expand Down
102 changes: 102 additions & 0 deletions build/parseSpec.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#define ISMACRO(s,m,len) (rstreqn((s), (m), len) && !risalpha((s)[len]))
#define ISMACROWITHARG(s,m,len) (rstreqn((s), (m), len) && (risblank((s)[len]) || !(s)[len]))

static rpmRC parseSpecParts(rpmSpec spec, const char *pattern);

typedef struct OpenFileInfo {
char * fileName;
FILE *fp;
Expand Down Expand Up @@ -894,6 +896,103 @@ static int parseEmpty(rpmSpec spec, int prevParsePart)
return res;
}

struct sectname_s {
const char *name;
int section;
};

struct sectname_s sectList[] = {
{ "prep", SECT_PREP },
{ "conf", SECT_CONF },
{ "generate_buildrequires", SECT_BUILDREQUIRES },
{ "build", SECT_BUILD },
{ "install", SECT_INSTALL },
{ "check", SECT_CHECK },
{ "clean", SECT_CLEAN },
{ NULL, -1 }
};

int getSection(const char *name)
{
int sn = -1;
for (struct sectname_s *sc = sectList; sc->name; sc++) {
if (rstreq(name, sc->name)) {
sn = sc->section;
break;
}
}
return sn;
}

static rpmRC parseBuildsysSect(rpmSpec spec, const char *prefix,
struct sectname_s *sc, FD_t fd)
{
rpmRC rc = RPMRC_OK;

if (spec->sections[sc->section] == NULL) {
char *mn = rstrscat(NULL, "buildsystem_", prefix, "_", sc->name, NULL);
if (rpmMacroIsParametric(NULL, mn)) {
char *args = NULL;
if (spec->buildopts[sc->section]) {
ARGV_t av = NULL;
argvAdd(&av, "--");
argvAppend(&av, spec->buildopts[sc->section]);
args = argvJoin(av, " ");
free(av);
}
char *buf = rstrscat(NULL, "%", sc->name, "\n",
"%", mn, " ",
args ? args : "",
"\n", NULL);
size_t blen = strlen(buf);
if (Fwrite(buf, blen, 1, fd) < blen) {
rc = RPMRC_FAIL;
rpmlog(RPMLOG_ERR,
_("failed to write buildsystem %s %%%s: %s\n"),
prefix, sc->name, strerror(errno));
}
free(buf);
free(args);
}
free(mn);
}
return rc;
}

static rpmRC parseBuildsystem(rpmSpec spec)
{
rpmRC rc = RPMRC_OK;
char *buildsystem = rpmExpand("%{?buildsystem}", NULL);
if (*buildsystem) {
char *path = NULL;

FD_t fd = rpmMkTempFile(NULL, &path);
if (fd == NULL) {
rpmlog(RPMLOG_ERR,
_("failed to create temp file for buildsystem: %s\n"),
strerror(errno));
rc = RPMRC_FAIL;
goto exit;
}

for (struct sectname_s *sc = sectList; !rc && sc->name; sc++) {
rc = parseBuildsysSect(spec, buildsystem, sc, fd);
if (!rc && spec->sections[sc->section] == NULL)
rc = parseBuildsysSect(spec, "default", sc, fd);
}

if (!rc)
rc = parseSpecParts(spec, path);
if (!rc)
unlink(path);
Fclose(fd);
}

exit:
free(buildsystem);
return rc;
}

static rpmSpec parseSpec(const char *specFile, rpmSpecFlags flags,
const char *buildRoot, int recursing);

Expand Down Expand Up @@ -1061,6 +1160,9 @@ static rpmRC parseSpecSection(rpmSpec *specptr, int secondary)
}
}

if (!secondary && parseBuildsystem(spec))
goto errxit;

/* Add arch for each package */
addArch(spec);

Expand Down
6 changes: 6 additions & 0 deletions build/rpmbuild_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ struct rpmSpec_s {
rpmstrPool pool;

StringBuf sections[NR_SECT]; /*!< spec sections (%prep etc) */
ARGV_t buildopts[NR_SECT]; /*!< per-section buildsystem options */

StringBuf parsed; /*!< parsed spec contents */

Package packages; /*!< Package list. */
Expand Down Expand Up @@ -639,6 +641,10 @@ void doSetupMacro(rpmMacroBuf mb, rpmMacroEntry me, ARGV_t margs, size_t *parsed
RPM_GNUC_INTERNAL
void doPatchMacro(rpmMacroBuf mb, rpmMacroEntry me, ARGV_t margs, size_t *parsed);

/* Return section number, -1 on error */
RPM_GNUC_INTERNAL
int getSection(const char *name);

#ifdef __cplusplus
}
#endif
Expand Down
115 changes: 115 additions & 0 deletions docs/manual/buildsystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
layout: default
title: rpm.org - Declarative builds
---

# Declarative builds

Most software follows common patterns as to how it should be prepared and
built, such as the classic `./configure && make && make install` case.
In a spec, these all go to their own sections and when combined with
the steps to unpack sources, it creates a lot of boilerplate that is
practically identical across a lot of specs. With the odd tweak to
this or that detail for distro preferences and the like. To reduce
the repetitive boilerplate, rpm >= 4.20 adds support for a declarative
build system mechanism where these common steps can be defined centrally
per each build system. Packagers then only need to declare which build
system they use, and optionally supply additional switches and/or
arguments for the steps where needed.

## Spec usage

In the most simple case, a spec will have an buildsystem declaration such
as `BuildSystem: cmake` and no manually specified build scripts at all.
However it's common to have to manually tweak a thing or two. There are
several ways how to accomplish this, what's appropriate depends on the
case.

1) Use append and/or prepend as necessary. Need to delete a file after
the install step? Add a `%install -a` section with the appropriate `rm`.
Need to `sed` something just before the build step? Add a `%build -p`
section with the necessary steps.

2) Another very common need is to pass extra arguments to the build
commands, build configuration in particular. This is done with the
BuildOption tag, which can appear arbitrary number of times
in the spec for each section.

```
BuildOption(<section>): <option string>
```

without the parenthesis defaults to configuration. In other words,
these two lines are exactly equivalent:

```
BuildOption: --enable-fu
BuildOption(conf): --enable-fu
```

Passing these per-section options to the actual buildsystem of the
package is the responsibility of the buildsystem specific macros.

3) Complex packages can have things like multiple build systems, in
which case you might want to invoke the macros manually, eg.

```
%buildsystem_autotools_build
cd python
%buildsystem_python_build
```

## Supporting new build systems

Supporting new build system types is just a matter of declaring a few
macros for the build scriptlet sections relevant to the build system.

Scriptlet | Mandatory | Buildsystem macro
-------------------------------------------
`%prep` | No | `%buildsystem_name_prep`
`%conf` | Yes | `%buildsystem_name_conf`
`%generate_buildrequires` | No | `%buildsystem_name_generate_buildrequires`
`%build` | Yes | `%buildsystem_name_build`
`%install` | Yes | `%buildsystem_name_install`
`%check` | No | `%buildsystem_name_check`
`%clean` | No | `%buildsystem_name_clean`

Replace "name" with the buildsystem name, eg `%buildsystem_cmake_build`.
When BuildSystem: tag is set, these automatically populate the corresponding
spec section, unless the spec manually overrides it. All buildsystem
macros are required to be parametric to have enforceable semantics.

For example, supporting the classic autotools case could be built on top
of existing helper macros:
```
%buildsystem_autotools_conf() %configure %*
%buildsystem_autotools_build() %make_build %*
%buildsystem_autotools_install() %make_install %*
```

## Global defaults

While the actual steps to build and install obviously differ greatly among
different buildsystems, they still typically share a lot of common steps.
Namely, unpacking sources, applying patches and cleaning up at the end.
To avoid each buildsystem having to declare a `%prep` to automatically
perform the same common duties, with inevitable subtle differences and
bugs, rpm additionally supports global default actions for all scriptlets
supported by the buildsystem mechanism. These defaults, if defined,
are only called if there's no corresponding buildsystem specific macro
defined.

Rpm ships with a default to perform `%autosetup -p1` in `%prep`,
so unless a buildsystem has an unusual source preparation sequence
source preparation will "just work". Passing extra arguments to a section
is exactly the same with defaults and buildsystem specific macros, so
the user does not need to know which one is being used. For example,
if the upstream source doesn' version their source release directory,
it can simply be supplied with the following in the spec:

```
BuildOption(prep): -n %{name}
```

Note that the defaults are only meant for upstream and distro-level
customization only, do not override them for your own purposes!
1 change: 1 addition & 0 deletions docs/manual/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ title: rpm.org - RPM Reference Manual
## Package Building
* [Build Process](buildprocess.md)
* [Spec Syntax](spec.md)
* [Autobuild](autobuild.md)
* [Autosetup](autosetup.md)
* Dependencies
* [Dependencies Basics](dependencies.md)
Expand Down
6 changes: 6 additions & 0 deletions docs/manual/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ with old packages that might still depend on it.

Do not use in new packages.

#### Buildsystem

Automatically populate the spec build scripts for the given build system,
such as `Buildsystem: autotools". See [declarative build](buildsystem.md)
documentation for more details.

#### AutoReqProv
#### AutoReq
#### AutoProv
Expand Down
2 changes: 2 additions & 0 deletions include/rpm/rpmtag.h
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ typedef enum rpmTag_e {
RPMTAG_PREUNTRANSFLAGS = 5107, /* i */
RPMTAG_POSTUNTRANSFLAGS = 5108, /* i */
RPMTAG_SYSUSERS = 5109, /* s[] extension */
RPMTAG_BUILDSYSTEM = 5110, /* internal */
RPMTAG_BUILDOPTION = 5111, /* internal */

RPMTAG_FIRSTFREE_TAG /*!< internal */
} rpmTag;
Expand Down
14 changes: 14 additions & 0 deletions macros.in
Original file line number Diff line number Diff line change
Expand Up @@ -1352,5 +1352,19 @@ end
end
}

# Global buildsystem defaults
%buildsystem_default_prep() %autosetup -p1 %*

# Example buildsystem for autotools
%buildsystem_autotools_conf() %configure %*
%buildsystem_autotools_build() %make_build %*
%buildsystem_autotools_install() %make_install %*

# Example buildsystem for cmake
%buildsystem_cmake_conf() cmake %* -B __rpmbuild -S .
%buildsystem_cmake_build() cmake --build __rpmbuild %{?smp_mflags} %{?verbose:-v} -- %*
%buildsystem_cmake_install() DESTDIR=${RPM_BUILD_ROOT} cmake --install __rpmbuild %{?verbose:-v} %*

# \endverbatim
#*/

Binary file added tests/data/SOURCES/amhello-1.0.tar.gz
Binary file not shown.
34 changes: 34 additions & 0 deletions tests/data/SPECS/amhello.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
%bcond_with alt

Name: amhello
Version: 1.0
Source: amhello-%{version}.tar.gz
License: GPLv2
Summary: Autotools example
BuildSystem: autotools

%if %{with alt}
BuildOption: --program-prefix=alt-
BuildOption(install): DESTDIR=${RPM_BUILD_ROOT}/alt
Release: 1alt
%else
Release: 1
%endif

%description
%{summary}

%build -a
cat << EOF > README.distro
Add some distro specific notes.
EOF

%files
%doc README.distro
%if %{with alt}
/alt/%{_bindir}/alt-hello
/alt/%{_docdir}/%{name}
%else
%{_bindir}/hello
%{_docdir}/%{name}
%endif
Loading

0 comments on commit f76123d

Please sign in to comment.