Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package product-composer for
openSUSE:Factory checked in at 2025-05-20 09:31:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/product-composer (Old)
and /work/SRC/openSUSE:Factory/.product-composer.new.30101 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "product-composer"
Tue May 20 09:31:35 2025 rev:36 rq:1278080 version:0.5.16
Changes:
--------
--- /work/SRC/openSUSE:Factory/product-composer/product-composer.changes
2025-05-12 16:55:29.473306160 +0200
+++
/work/SRC/openSUSE:Factory/.product-composer.new.30101/product-composer.changes
2025-05-20 09:31:44.053299490 +0200
@@ -1,0 +2,21 @@
+Fri May 16 13:28:58 UTC 2025 - Adrian Schröter <[email protected]>
+
+- update to version 0.5.16:
+ * package EULA support added
+ * agama: do not take the iso meta data from the agama iso
+ * code cleanup and refactoring
+ * build description files are now validated.
+ * verify command is now checking all flavors by default.
+
+-------------------------------------------------------------------
+Tue May 13 12:34:06 UTC 2025 - Adrian Schröter <[email protected]>
+
+- update to version 0.5.15:
+ * fix generation of gpg-pubkey content tags
+ * Do not error out in updateinfo_packages_only mode if packages are not found
+ * Set BUILD_DIR before calling the sbom generator
+ * Handle build_options in flavors different
+ Add them to the global set, instead of replacing the global set.
+ * Fix handover of multiple --build-option cli parameters
+
+-------------------------------------------------------------------
Old:
----
_service
product-composer-0.5.14.obscpio
product-composer.obsinfo
New:
----
_scmsync.obsinfo
build.specials.obscpio
product-composer.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ product-composer.spec ++++++
--- /var/tmp/diff_new_pack.udBrNn/_old 2025-05-20 09:31:46.329393140 +0200
+++ /var/tmp/diff_new_pack.udBrNn/_new 2025-05-20 09:31:46.337393469 +0200
@@ -23,12 +23,13 @@
%endif
Name: product-composer
-Version: 0.5.14
+Version: 0.5.16
Release: 0
Summary: Product Composer
License: GPL-2.0-or-later
Group: Development/Tools/Building
URL: https://github.com/openSUSE/product-composer
+#!CreateArchive: product-composer
Source: %name-%{version}.tar.xz
# Should become a build option
Patch1: sle-15-defaults.patch
++++++ _scmsync.obsinfo ++++++
mtime: 1747402289
commit: fed5fbb26fa9c09ab6729daf6640a03d7723158f7ac9954c016c1bc9eb55bf00
url: https://src.opensuse.org/tools/product-composer
revision: devel
++++++ product-composer-0.5.14.obscpio -> product-composer.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/product-composer-0.5.14/.git
new/product-composer/.git
--- old/product-composer-0.5.14/.git 1970-01-01 01:00:00.000000000 +0100
+++ new/product-composer/.git 2025-05-16 15:31:50.000000000 +0200
@@ -0,0 +1 @@
+gitdir: ../.git/modules/product-composer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/product-composer-0.5.14/.github/workflows/tests.yaml
new/product-composer/.github/workflows/tests.yaml
--- old/product-composer-0.5.14/.github/workflows/tests.yaml 2025-05-08
12:37:30.000000000 +0200
+++ new/product-composer/.github/workflows/tests.yaml 2025-05-16
15:31:50.000000000 +0200
@@ -10,7 +10,7 @@
jobs:
unit:
- name: "unit"
+ name: "basic"
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
@@ -26,12 +26,13 @@
run: |
zypper -n modifyrepo --disable repo-openh264 || :
zypper -n --gpg-auto-import-keys refresh
- zypper -n install python3 python3-pip python3-pydantic
python3-pytest python3-rpm python3-setuptools python3-solv python3-PyYAML
+ zypper -n install python3 python3-pip python3-pydantic
python3-pytest python3-rpm python3-setuptools python3-solv python3-PyYAML
python3-schema
- uses: actions/checkout@v4
- - name: 'Run unit tests'
+ - name: 'Run basic example verification'
run: |
pip3 config set global.break-system-packages 1
pip3 install --no-dependencies -e .
- pytest tests
+ productcomposer verify examples/ftp.productcompose
+# pytest tests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/product-composer-0.5.14/README.rst
new/product-composer/README.rst
--- old/product-composer-0.5.14/README.rst 2025-05-08 12:37:30.000000000
+0200
+++ new/product-composer/README.rst 2025-05-16 15:31:50.000000000 +0200
@@ -5,7 +5,8 @@
repositories inside of Open Build Service based on a larger pool
of packages.
-It is starting as small as possible, just enough for ALP products atm.
+It is used by any SLFO based product during product creation and
+also during maintenance time.
Currently it supports:
- processing based on a list of rpm package names
@@ -13,9 +14,8 @@
- it can either just take a single rpm of a given name or all of them
- it can post process updateinfo data
- post processing like rpm meta data generation
-
-Not yet implemented:
- - create bootable iso files
+ - modify pre-generated installer images to put a package set for
+ off-line installation on it.
Development
===========
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/product-composer-0.5.14/examples/ftp.productcompose
new/product-composer/examples/ftp.productcompose
--- old/product-composer-0.5.14/examples/ftp.productcompose 2025-05-08
12:37:30.000000000 +0200
+++ new/product-composer/examples/ftp.productcompose 2025-05-16
15:31:50.000000000 +0200
@@ -24,10 +24,10 @@
# free: false
iso:
- publisher:
- volume_id:
-# tree: drop
-# base: agama-installer
+ publisher: 'Iggy'
+ volume_id: 'Pop'
+# tree: 'drop'
+# base: 'agama-installer'
build_options:
### For maintenance, otherwise only "the best" version of each package is
picked:
@@ -40,8 +40,8 @@
# - updateinfo_packages_only
# - base_skip_packages
-#installcheck:
-# - ignore_errors
+installcheck:
+ - ignore_errors
# Enable collection of source and debug packages. Either "include" it
# on main medium, "drop" it or "split" it away on extra medium.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/product-composer-0.5.14/pyproject.toml
new/product-composer/pyproject.toml
--- old/product-composer-0.5.14/pyproject.toml 2025-05-08 12:37:30.000000000
+0200
+++ new/product-composer/pyproject.toml 2025-05-16 15:31:50.000000000 +0200
@@ -12,6 +12,7 @@
"zstandard",
"pydantic<2",
"pyyaml",
+ "schema",
]
dynamic = ["version", "readme"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/product-composer-0.5.14/src/productcomposer/api/parse.py
new/product-composer/src/productcomposer/api/parse.py
--- old/product-composer-0.5.14/src/productcomposer/api/parse.py
2025-05-08 12:37:30.000000000 +0200
+++ new/product-composer/src/productcomposer/api/parse.py 2025-05-16
15:31:50.000000000 +0200
@@ -10,4 +10,4 @@
:param name: name to use in greeting
"""
logger.debug("executing hello command")
- return f"Hello, parser!"
+ return "Hello, parser!"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/product-composer-0.5.14/src/productcomposer/cli.py
new/product-composer/src/productcomposer/cli.py
--- old/product-composer-0.5.14/src/productcomposer/cli.py 2025-05-08
12:37:30.000000000 +0200
+++ new/product-composer/src/productcomposer/cli.py 2025-05-16
15:31:50.000000000 +0200
@@ -7,10 +7,12 @@
import shutil
import subprocess
import gettext
+import glob
from datetime import datetime
from argparse import ArgumentParser
from xml.etree import ElementTree as ET
+from schema import Schema, And, Or, Optional, SchemaError
import yaml
from .core.logger import logger
@@ -25,6 +27,8 @@
ET_ENCODING = "unicode"
+ISO_PREPARER = "Product Composer -
http://www.github.com/openSUSE/product-composer"
+DEFAULT_EULADIR = "/usr/share/doc/packages/eulas"
tree_report = {} # hashed via file name
@@ -34,14 +38,118 @@
# global db for supportstatus
supportstatus = {}
+# global db for eulas
+eulas = {}
# per package override via supportstatus.txt file
supportstatus_override = {}
# debug aka verbose
verbose_level = 0
+compose_schema_iso = Schema({
+ Optional('publisher'): str,
+ Optional('volume_id'): str,
+ Optional('tree'): str,
+ Optional('base'): str,
+})
+compose_schema_packageset = Schema({
+ Optional('name'): str,
+ Optional('supportstatus'): str,
+ Optional('flavors'): [str],
+ Optional('architectures'): [str],
+ Optional('add'): [str],
+ Optional('sub'): [str],
+ Optional('intersect'): [str],
+ Optional('packages'): Or(None, [str]),
+})
+compose_schema_scc_cpe = Schema({
+ 'cpe': str,
+ Optional('online'): bool,
+})
+compose_schema_scc = Schema({
+ Optional('description'): str,
+ Optional('family'): str,
+ Optional('product-class'): str,
+ Optional('free'): bool,
+ Optional('predecessors'): [compose_schema_scc_cpe],
+ Optional('shortname'): str,
+ Optional('base-products'): [compose_schema_scc_cpe],
+ Optional('root-products'): [compose_schema_scc_cpe],
+ Optional('recommended-for'): [compose_schema_scc_cpe],
+ Optional('migration-extra-for'): [compose_schema_scc_cpe],
+})
+compose_schema_build_option = Schema(
+ Or(
+ 'add_slsa_provenance',
+ 'base_skip_packages',
+ 'block_updates_under_embargo',
+ 'hide_flavor_in_product_directory_name',
+ 'ignore_missing_packages',
+ 'skip_updateinfos',
+ 'take_all_available_versions',
+ 'updateinfo_packages_only',
+ )
+)
+compose_schema_source_and_debug = Schema(
+ Or(
+ 'drop',
+ 'include',
+ 'split',
+ )
+)
+compose_schema_repodata = Schema(
+ Or(
+ 'all',
+ 'split',
+ )
+)
+compose_schema_flavor = Schema({
+ Optional('architectures'): [str],
+ Optional('name'): str,
+ Optional('version'): str,
+ Optional('update'): str,
+ Optional('edition'): str,
+ Optional('product-type'): str,
+ Optional('product_directory_name'): str,
+ Optional('repodata'): compose_schema_repodata,
+ Optional('summary'): str,
+ Optional('debug'): compose_schema_source_and_debug,
+ Optional('source'): compose_schema_source_and_debug,
+ Optional('build_options'): Or(None, [compose_schema_build_option]),
+ Optional('scc'): compose_schema_scc,
+ Optional('iso'): compose_schema_iso,
+})
+
+compose_schema = Schema({
+ 'product_compose_schema': str,
+ 'vendor': str,
+ 'name': str,
+ 'version': str,
+ Optional('update'): str,
+ 'product-type': str,
+ 'summary': str,
+ Optional('bcntsynctag'): str,
+ Optional('milestone'): str,
+ Optional('scc'): compose_schema_scc,
+ Optional('iso'): compose_schema_iso,
+ Optional('installcheck'): Or(None, ['ignore_errors']),
+ Optional('build_options'): Or(None, [compose_schema_build_option]),
+ Optional('architectures'): [str],
+
+ Optional('product_directory_name'): str,
+ Optional('set_updateinfo_from'): str,
+ Optional('set_updateinfo_id_prefix'): str,
+ Optional('block_updates_under_embargo'): str,
+ Optional('debug'): compose_schema_source_and_debug,
+ Optional('source'): compose_schema_source_and_debug,
+ Optional('repodata'): compose_schema_repodata,
+
+ Optional('flavors'): {str: compose_schema_flavor},
+ Optional('packagesets'): [compose_schema_packageset],
+ Optional('unpack'): [str],
+})
def main(argv=None) -> int:
- """ Execute the application CLI.
+ """Execute the application CLI.
:param argv: argument list to parse (sys.argv by default)
:return: exit status
@@ -60,7 +168,7 @@
build_parser.set_defaults(func=build)
# Generic options
- for cmd_parser in [verify_parser, build_parser]:
+ for cmd_parser in (verify_parser, build_parser):
cmd_parser.add_argument('-f', '--flavor', help='Build a given flavor')
cmd_parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose output')
cmd_parser.add_argument('--reposdir', action='store', help='Take
packages from this directory')
@@ -69,9 +177,10 @@
# build command options
build_parser.add_argument('-r', '--release', default=None, help='Define a
build release counter')
build_parser.add_argument('--disturl', default=None, help='Define a
disturl')
- build_parser.add_argument('--build-option', nargs='+', default=[],
help='Set a build option')
+ build_parser.add_argument('--build-option', action='append', nargs='+',
default=[], help='Set a build option')
build_parser.add_argument('--vcs', default=None, help='Define a source
repository identifier')
build_parser.add_argument('--clean', action='store_true', help='Remove
existing output directory first')
+ build_parser.add_argument('--euladir', default=DEFAULT_EULADIR,
help='Directory containing EULA data')
build_parser.add_argument('out', help='Directory to write the result')
# parse and check
@@ -110,6 +219,8 @@
def build(args):
flavor = None
+ global verbose_level
+
if args.flavor:
f = args.flavor.split('.')
if f[0] != '':
@@ -118,15 +229,16 @@
verbose_level = 1
if not args.out:
- # No subcommand was specified.
- print("No output directory given")
- parser.print_help()
- die(None)
+ die("No output directory given")
yml = parse_yaml(args.filename, flavor)
- for option in args.build_option:
- yml['build_options'].append(option)
+ for arg in args.build_option:
+ for option in arg:
+ yml['build_options'].append(option)
+
+ if 'architectures' not in yml or not yml['architectures']:
+ die(f'No architecture defined for flavor {flavor}')
directory = os.getcwd()
if args.filename.startswith('/'):
@@ -137,22 +249,25 @@
if os.path.isfile(supportstatus_fn):
parse_supportstatus(supportstatus_fn)
+ if args.euladir and os.path.isdir(args.euladir):
+ parse_eulas(args.euladir)
+
pool = Pool()
- note(f"scanning: {reposdir}")
+ note(f"Scanning: {reposdir}")
pool.scan(reposdir)
- # clean up black listed packages
+ # clean up blacklisted packages
for u in sorted(pool.lookup_all_updateinfos()):
for update in u.root.findall('update'):
if not update.find('blocked_in_product'):
- continue
+ continue
parent = update.findall('pkglist')[0].findall('collection')[0]
for pkgentry in parent.findall('package'):
name = pkgentry.get('name')
epoch = pkgentry.get('epoch')
version = pkgentry.get('version')
- pool.remove_rpms(None, name, '=', epoch, version)
+ pool.remove_rpms(None, name, '=', epoch, version, None)
if args.clean and os.path.exists(args.out):
shutil.rmtree(args.out)
@@ -163,50 +278,83 @@
def verify(args):
- parse_yaml(args.filename, args.flavor)
+ yml = parse_yaml(args.filename, args.flavor)
+ if args.flavor == None and 'flavors' in yml:
+ for flavor in yml['flavors']:
+ yml = parse_yaml(args.filename, flavor)
+ if 'architectures' not in yml or not yml['architectures']:
+ die(f'No architecture defined for flavor {flavor}')
+ elif 'architectures' not in yml or not yml['architectures']:
+ die('No architecture defined and no flavor.')
-def parse_yaml(filename, flavor):
+def parse_yaml(filename, flavor):
with open(filename, 'r') as file:
yml = yaml.safe_load(file)
+ # we may not allow this in future anymore, but for now convert these from
float to str
+ if 'product_compose_schema' in yml:
+ yml['product_compose_schema'] = str(yml['product_compose_schema'])
+ if 'version' in yml:
+ yml['version'] = str(yml['version'])
+
if 'product_compose_schema' not in yml:
die('missing product composer schema')
- if yml['product_compose_schema'] != 0 and yml['product_compose_schema'] !=
0.1 and yml['product_compose_schema'] != 0.2:
- die(f"Unsupported product composer schema:
{yml['product_compose_schema']}")
+ if yml['product_compose_schema'] not in ('0.1', '0.2'):
+ die(f'Unsupported product composer schema:
{yml["product_compose_schema"]}')
+
+ try:
+ compose_schema.validate(yml)
+ note(f"Configuration is valid for flavor: {flavor}")
+ except SchemaError as se:
+ warn(f"YAML syntax is invalid for flavor: {flavor}")
+ raise se
if 'flavors' not in yml:
yml['flavors'] = []
+ if 'build_options' not in yml or yml['build_options'] is None:
+ yml['build_options'] = []
+
if flavor:
if flavor not in yml['flavors']:
- die("Flavor not found: " + flavor)
+ die('Flavor not found: ' + flavor)
f = yml['flavors'][flavor]
# overwrite global values from flavor overwrites
- for tag in ['architectures', 'name', 'summary', 'version', 'update',
'edition',
- 'product-type', 'product_directory_name',
- 'build_options', 'source', 'debug', 'repodata']:
+ for tag in (
+ 'architectures',
+ 'name',
+ 'summary',
+ 'version',
+ 'update',
+ 'edition',
+ 'product-type',
+ 'product_directory_name',
+ 'source',
+ 'debug',
+ 'repodata',
+ ):
if tag in f:
yml[tag] = f[tag]
+
+ # Add additional build_options instead of replacing global defined set.
+ if 'build_options' in f:
+ for option in f['build_options']:
+ yml['build_options'].append(option)
+
if 'iso' in f:
- if not 'iso' in yml:
+ if 'iso' not in yml:
yml['iso'] = {}
- for tag in ['volume_id', 'publisher', 'tree', 'base']:
+ for tag in ('volume_id', 'publisher', 'tree', 'base'):
if tag in f['iso']:
yml['iso'][tag] = f['iso'][tag]
- if 'architectures' not in yml or not yml['architectures']:
- die("No architecture defined. Maybe wrong flavor?")
-
- if 'build_options' not in yml or yml['build_options'] is None:
- yml['build_options'] = []
-
if 'installcheck' in yml and yml['installcheck'] is None:
yml['installcheck'] = []
# FIXME: validate strings, eg. right set of chars
-
+
return yml
@@ -217,26 +365,39 @@
supportstatus_override[a[0]] = a[1]
+def parse_eulas(euladir):
+ note(f"Reading eula data from {euladir}")
+ for dirpath, dirs, files in os.walk(euladir):
+ for filename in files:
+ if filename.startswith('.'):
+ continue
+ pkgname = filename.removesuffix('.en')
+ with open(os.path.join(dirpath, filename), encoding="utf-8") as f:
+ eulas[pkgname] = f.read()
+
+
def get_product_dir(yml, flavor, release):
- name = yml['name'] + "-" + str(yml['version'])
+ name = f'{yml["name"]}-{yml["version"]}'
if 'product_directory_name' in yml:
# manual override
name = yml['product_directory_name']
- if flavor and not 'hide_flavor_in_product_directory_name' in
yml['build_options']:
- name += "-" + flavor
+ if flavor and 'hide_flavor_in_product_directory_name' not in
yml['build_options']:
+ name += f'-{flavor}'
if yml['architectures']:
visible_archs = yml['architectures']
if 'local' in visible_archs:
visible_archs.remove('local')
name += "-" + "-".join(visible_archs)
if release:
- name += "-Build" + str(release)
+ name += f'-Build{release}'
if '/' in name:
die("Illegal product name")
return name
-def run_helper(args, cwd=None, fatal=True, stdout=None, stdin=None,
failmsg=None):
+def run_helper(args, cwd=None, fatal=True, stdout=None, stdin=None,
failmsg=None, verbose=False):
+ if verbose:
+ note(f'Calling {args}')
if stdout is None:
stdout = subprocess.PIPE
if stdin is None:
@@ -264,6 +425,71 @@
args = [ 'sha256sum', filename.split('/')[-1] ]
run_helper(args, cwd=("/"+os.path.join(*filename.split('/')[:-1])),
stdout=sha_file, failmsg="create .sha256 file")
+def create_iso(outdir, yml, pool, flavor, workdir, application_id):
+ verbose = True if verbose_level > 0 else False
+ isoconf = yml['iso']
+ args = ['/usr/bin/mkisofs', '-quiet', '-p', ISO_PREPARER]
+ args += ['-r', '-pad', '-f', '-J', '-joliet-long']
+ if 'publisher' in isoconf and isoconf['publisher'] is not None:
+ args += ['-publisher', isoconf['publisher']]
+ if 'volume_id' in isoconf and isoconf['volume_id'] is not None:
+ args += ['-V', isoconf['volume_id']]
+ args += ['-A', application_id]
+ args += ['-o', workdir + '.iso', workdir]
+ run_helper(args, cwd=outdir, failmsg="create iso file", verbose=verbose)
+ # simple tag media call ... we may add options for pading or triggering
media check later
+ args = [ 'tagmedia' , '--digest' , 'sha256', workdir + '.iso' ]
+ run_helper(args, cwd=outdir, failmsg="tagmedia iso file", verbose=verbose)
+ # creating .sha256 for iso file
+ create_sha256_for(workdir + ".iso")
+
+def create_agama_iso(outdir, yml, pool, flavor, workdir, application_id, arch):
+ verbose = True if verbose_level > 0 else False
+ isoconf = yml['iso']
+ base = isoconf['base']
+ if verbose:
+ note(f"Looking for baseiso-{base} rpm on {arch}")
+ agama = pool.lookup_rpm(arch, f"baseiso-{base}")
+ if not agama:
+ die(f"Base iso in baseiso-{base} rpm was not found")
+ baseisodir = f"{outdir}/baseiso"
+ os.mkdir(baseisodir)
+ args = ['unrpm', '-q', agama.location]
+ run_helper(args, cwd=baseisodir, failmsg=f"extract {agama.location}",
verbose=verbose)
+ files = glob.glob(f"usr/libexec/base-isos/{base}*.iso",
root_dir=baseisodir)
+ if not files:
+ die(f"Base iso {base} not found in {agama}")
+ if len(files) > 1:
+ die(f"Multiple base isos for {base} found in {agama}")
+ agamaiso = f"{baseisodir}/{files[0]}"
+ if verbose:
+ note(f"Found base iso image {agamaiso}")
+
+ # create new iso
+ tempdir = f"{outdir}/mksusecd"
+ os.mkdir(tempdir)
+ if 'base_skip_packages' not in yml['build_options']:
+ args = ['cp', '-al', workdir, f"{tempdir}/install"]
+ run_helper(args, failmsg="add tree to agama image")
+ args = ['mksusecd', agamaiso, tempdir, '--create', workdir +
'.install.iso']
+ # mksusecd would take the volume_id, publisher, application_id, preparer
from the agama iso
+ args += ['--preparer', ISO_PREPARER]
+ if 'publisher' in isoconf and isoconf['publisher'] is not None:
+ args += ['--vendor', isoconf['publisher']]
+ if 'volume_id' in isoconf and isoconf['volume_id'] is not None:
+ args += ['--volume', isoconf['volume_id']]
+ args += ['--application', application_id]
+ run_helper(args, failmsg="add tree to agama image", verbose=verbose)
+ # mksusecd already did a tagmedia call with a sha256 digest
+ # cleanup directories
+ shutil.rmtree(tempdir)
+ shutil.rmtree(baseisodir)
+ # just for the bootable image, signature is not yet applied, so ignore
that error
+ run_helper(['verifymedia', workdir + '.install.iso', '--ignore', 'ISO is
signed'], fatal=False, failmsg="verify install.iso")
+ # creating .sha256 for iso file
+ create_sha256_for(workdir + '.install.iso')
+
+
def create_tree(outdir, product_base_dir, yml, pool, flavor, vcs=None,
disturl=None):
if not os.path.exists(outdir):
os.mkdir(outdir)
@@ -272,7 +498,7 @@
if not os.path.exists(maindir):
os.mkdir(maindir)
- workdirectories = [ maindir ]
+ workdirectories = [maindir]
debugdir = sourcedir = None
if "source" in yml:
if yml['source'] == 'split':
@@ -320,12 +546,11 @@
args = ['gpg', '--no-keyring', '--no-default-keyring', '--with-colons',
'--import-options', 'show-only', '--import', '--fingerprint']
out = run_helper(args, stdin=open(f'{maindir}/{file}', 'rb'),
- failmsg="Finger printing of gpg file")
+ failmsg="get fingerprint of gpg file")
for line in out.splitlines():
- if not str(line).startswith("b'fpr:"):
- continue
-
- default_content.append(str(line).split(':')[9])
+ if line.startswith("fpr:"):
+ content = f"{file}?fpr={line.split(':')[9]}"
+ default_content.append(content)
note("Create rpm-md data")
run_createrepo(maindir, yml, content=default_content, repos=repos)
@@ -394,39 +619,40 @@
args.append(find_primary(maindir + subdir))
if debugdir:
args.append(find_primary(debugdir + subdir))
- run_helper(args, fatal=(not 'ignore_errors' in
yml['installcheck']), failmsg="run installcheck validation")
+ run_helper(args, fatal=('ignore_errors' not in
yml['installcheck']), failmsg="run installcheck validation")
if 'skip_updateinfos' not in yml['build_options']:
create_updateinfo_xml(maindir, yml, pool, flavor, debugdir, sourcedir)
# Add License File and create extra .license directory
- licensefilename = '/license.tar'
- if os.path.exists(maindir + '/license-' + yml['name'] + '.tar') or
os.path.exists(maindir + '/license-' + yml['name'] + '.tar.gz'):
- licensefilename = '/license-' + yml['name'] + '.tar'
- if os.path.exists(maindir + licensefilename + '.gz'):
- run_helper(['gzip', '-d', maindir + licensefilename + '.gz'],
- failmsg="Uncompress of license.tar.gz failed")
- if os.path.exists(maindir + licensefilename):
- note("Setup .license directory")
- licensedir = maindir + ".license"
- if not os.path.exists(licensedir):
- os.mkdir(licensedir)
- args = ['tar', 'xf', maindir + licensefilename, '-C', licensedir]
- output = run_helper(args, failmsg="extract license tar ball")
- if not os.path.exists(licensedir + "/license.txt"):
- die("No license.txt extracted", details=output)
-
- mr = ModifyrepoWrapper(
- file=maindir + licensefilename,
- directory=os.path.join(maindir, "repodata"),
- )
- mr.run_cmd()
- os.unlink(maindir + licensefilename)
- # meta package may bring a second file or expanded symlink, so we need
clean up
- if os.path.exists(maindir + '/license.tar'):
- os.unlink(maindir + '/license.tar')
- if os.path.exists(maindir + '/license.tar.gz'):
- os.unlink(maindir + '/license.tar.gz')
+ if yml.get('iso', {}).get('tree') != 'drop':
+ licensefilename = '/license.tar'
+ if os.path.exists(maindir + '/license-' + yml['name'] + '.tar') or
os.path.exists(maindir + '/license-' + yml['name'] + '.tar.gz'):
+ licensefilename = '/license-' + yml['name'] + '.tar'
+ if os.path.exists(maindir + licensefilename + '.gz'):
+ run_helper(['gzip', '-d', maindir + licensefilename + '.gz'],
+ failmsg="uncompress license.tar.gz")
+ if os.path.exists(maindir + licensefilename):
+ note("Setup .license directory")
+ licensedir = maindir + ".license"
+ if not os.path.exists(licensedir):
+ os.mkdir(licensedir)
+ args = ['tar', 'xf', maindir + licensefilename, '-C', licensedir]
+ output = run_helper(args, failmsg="extract license tar ball")
+ if not os.path.exists(licensedir + "/license.txt"):
+ die("No license.txt extracted", details=output)
+
+ mr = ModifyrepoWrapper(
+ file=maindir + licensefilename,
+ directory=os.path.join(maindir, "repodata"),
+ )
+ mr.run_cmd()
+ os.unlink(maindir + licensefilename)
+ # meta package may bring a second file or expanded symlink, so we
need clean up
+ if os.path.exists(maindir + '/license.tar'):
+ os.unlink(maindir + '/license.tar')
+ if os.path.exists(maindir + '/license.tar.gz'):
+ os.unlink(maindir + '/license.tar.gz')
for repodatadir in repodatadirectories:
# detached signature
@@ -443,68 +669,18 @@
args = ['/usr/lib/build/signdummy', '-d', workdir + '/CHECKSUMS']
run_helper(args, failmsg="create detached signature for CHECKSUMS")
+ application_id = product_base_dir
# When using the baseiso feature, the primary media should be
# the base iso, with the packages added.
# Other medias/workdirs would then be generated as usual, as
# presumably you wouldn't need a bootable iso for source and
# debuginfo packages.
if workdir == maindir and 'base' in yml.get('iso', {}):
- note("Export main tree into agama iso file")
- if verbose_level > 0:
- note(f"Looking for baseiso-{yml['iso']['base']} rpm on
{yml['architectures'][0]}")
- agama = pool.lookup_rpm(yml['architectures'][0],
f"baseiso-{yml['iso']['base']}")
- if agama is None:
- die(f"Base iso in baseiso-{yml['iso']['base']} rpm was not
found")
- if verbose_level > 0:
- note(f"Found {agama.location}")
- baseisodir = f"{outdir}/baseiso"
- os.mkdir(baseisodir)
- args = ['unrpm', '-q', agama.location]
- run_helper(args, cwd=baseisodir, failmsg=f"Failing unpacking
{agama.location}")
- import glob
- files =
glob.glob(f"{baseisodir}/usr/libexec/base-isos/{yml['iso']['base']}*.iso",
recursive=True)
- if len(files) < 1:
- die(f"Base iso {yml['iso']['base']} not found in
{agama.location}")
- if len(files) > 1:
- die(f"Multiple base isos for {yml['iso']['base']} are found in
{agama.location}")
- agamaiso = files[0]
- if verbose_level > 0:
- note(f"Found base iso image {agamaiso}")
-
- # create new iso
- tempdir = f"{outdir}/mksusecd"
- os.mkdir(tempdir)
- if not 'base_skip_packages' in yml['build_options']:
- args = ['cp', '-al', workdir, f"{tempdir}/install"]
- run_helper(args, failmsg="Adding tree to agama image")
- args = ['mksusecd', agamaiso, tempdir, '--create', workdir +
'.install.iso']
- if verbose_level > 0:
- print("Calling: ", args)
- run_helper(args, failmsg="Adding tree to agama image")
- # just for the bootable image, signature is not yet applied, so
ignore that error
- run_helper(['verifymedia', workdir + '.install.iso', '--ignore',
'ISO is signed'], fatal=False, failmsg="Verification of install.iso")
- # creating .sha256 for iso file
- create_sha256_for(workdir + '.install.iso')
- # cleanup
- shutil.rmtree(tempdir)
- shutil.rmtree(baseisodir)
+ agama_arch = yml['architectures'][0]
+ note(f"Export main tree into agama iso file for {agama_arch}")
+ create_agama_iso(outdir, yml, pool, flavor, workdir,
application_id, agama_arch)
elif 'iso' in yml:
- note("Create iso files")
- application_id = re.sub(r'^.*/', '', maindir)
- args = ['/usr/bin/mkisofs', '-quiet', '-p', 'Product Composer -
http://www.github.com/openSUSE/product-composer']
- args += ['-r', '-pad', '-f', '-J', '-joliet-long']
- if 'publisher' in yml['iso'] and yml['iso']['publisher'] is not
None:
- args += ['-publisher', yml['iso']['publisher']]
- if 'volume_id' in yml['iso'] and yml['iso']['volume_id'] is not
None:
- args += ['-V', yml['iso']['volume_id']]
- args += ['-A', application_id]
- args += ['-o', workdir + '.iso', workdir]
- run_helper(args, cwd=outdir, failmsg="create iso file")
- # simple tag media call ... we may add options for pading or
triggering media check later
- args = [ 'tagmedia' , '--digest' , 'sha256', workdir + '.iso' ]
- run_helper(args, cwd=outdir, failmsg="tagmedia iso file")
- # creating .sha256 for iso file
- create_sha256_for(workdir + ".iso")
+ create_iso(outdir, yml, pool, flavor, workdir, application_id);
# cleanup
if yml.get('iso', {}).get('tree') == 'drop':
@@ -520,7 +696,7 @@
# Pro: SBOM formats are constant changing, we don't need to adapt always
all distributions for that
if os.path.exists("/.build/generate_sbom"):
# unfortunatly, it is not exectuable by default
- generate_sbom_call = ["perl", "-I", "/.build", "/.build/generate_sbom"]
+ generate_sbom_call = ['env', 'BUILD_DIR=/.build', 'perl',
'/.build/generate_sbom']
if generate_sbom_call:
spdx_distro = f"{yml['name']}-{yml['version']}"
@@ -573,19 +749,22 @@
for root, dirnames, filenames in os.walk(maindir + '/' + subdir):
for name in filenames:
relname = os.path.relpath(root + '/' + name, maindir)
- run_helper([chksums_tool, relname], cwd=maindir,
stdout=chksums_file)
+ run_helper(
+ [chksums_tool, relname], cwd=maindir,
stdout=chksums_file
+ )
+
# create a fake package entry from an updateinfo package spec
def create_updateinfo_package(pkgentry):
entry = Package()
- for tag in 'name', 'epoch', 'version', 'release', 'arch':
+ for tag in ('name', 'epoch', 'version', 'release', 'arch'):
setattr(entry, tag, pkgentry.get(tag))
return entry
+
def generate_du_data(pkg, maxdepth):
- dirs = pkg.get_directories()
seen = set()
dudata_size = {}
dudata_count = {}
@@ -622,6 +801,7 @@
dudata.append((dir, size, dudata_count[dir]))
return dudata
+
# Get supported translations based on installed packages
def get_package_translation_languages():
i18ndir = '/usr/share/locale/en_US/LC_MESSAGES'
@@ -699,6 +879,11 @@
for duitem in dudata:
ET.SubElement(dirselement, 'dir', {'name': duitem[0],
'size': str(duitem[1]), 'count': str(duitem[2])})
+ # add eula
+ eula = eulas.get(name)
+ if eula:
+ ET.SubElement(package, 'eula').text = eula
+
# get summary/description/category of the package
summary = pkg.find(f'{ns}summary').text
description = pkg.find(f'{ns}description').text
@@ -710,7 +895,8 @@
isummary = i18ntrans[lang].gettext(summary)
idescription = i18ntrans[lang].gettext(description)
icategory = i18ntrans[lang].gettext(category) if category is not
None else None
- if isummary == summary and idescription == description and
icategory == category:
+ ieula = eulas.get(name + '.' + lang, eula) if eula is not None
else None
+ if isummary == summary and idescription == description and
icategory == category and ieula == eula:
continue
if lang not in susedatas:
susedatas[lang] = ET.Element('susedata')
@@ -724,6 +910,8 @@
ET.SubElement(ipackage, 'description', {'lang': lang}).text =
idescription
if icategory != category:
ET.SubElement(ipackage, 'category', {'lang': lang}).text =
icategory
+ if ieula != eula:
+ ET.SubElement(ipackage, 'eula', {'lang': lang}).text = ieula
# write all susedata files
for lang, susedata in sorted(susedatas.items()):
@@ -753,7 +941,7 @@
# build the union of the package sets for all requested architectures
main_pkgset = PkgSet('main')
for arch in yml['architectures']:
- pkgset = main_pkgset.add(create_package_set(yml, arch, flavor, 'main',
pool=pool))
+ main_pkgset.add(create_package_set(yml, arch, flavor, 'main',
pool=pool))
main_pkgset_names = main_pkgset.names()
uitemp = None
@@ -794,7 +982,11 @@
die("shutting down due to
block_updates_under_embargo flag")
# clean internal attributes
- for internal_attributes in ['supportstatus', 'superseded_by',
'embargo_date']:
+ for internal_attributes in (
+ 'supportstatus',
+ 'superseded_by',
+ 'embargo_date',
+ ):
pkgentry.attrib.pop(internal_attributes, None)
# check if we have files for the entry
@@ -853,16 +1045,10 @@
os.unlink(rpmdir + '/updateinfo.xml')
- if missing_package and not 'ignore_missing_packages' in
yml['build_options']:
- die('Abort due to missing packages')
-
+ if missing_package and 'ignore_missing_packages' not in
yml['build_options']:
+ die('Abort due to missing packages for updateinfo')
def run_createrepo(rpmdir, yml, content=[], repos=[]):
- product_name = product_summary = yml['name']
- if 'summary' in yml:
- product_summary = yml['summary']
- product_summary += " " + str(yml['version'])
-
product_type = '/o'
if 'product-type' in yml:
if yml['product-type'] == 'base':
@@ -872,7 +1058,7 @@
else:
die('Undefined product-type')
cr = CreaterepoWrapper(directory=".")
- cr.distro = product_summary
+ cr.distro = f"{yml.get('summary', yml['name'])} {yml['version']}"
cr.cpeid =
f"cpe:{product_type}:{yml['vendor']}:{yml['name']}:{yml['version']}"
if 'update' in yml:
cr.cpeid = cr.cpeid + f":{yml['update']}"
@@ -881,7 +1067,7 @@
elif 'edition' in yml:
cr.cpeid = cr.cpeid + f"::{yml['edition']}"
cr.repos = repos
-# cr.split = True
+ # cr.split = True
# cr.baseurl = "media://"
cr.content = content
cr.excludes = ["boot"]
@@ -920,32 +1106,8 @@
continue
unpack_one_meta_rpm(rpmdir, rpm, medium)
- if missing_package and not 'ignore_missing_packages' in
yml['build_options']:
- die('Abort due to missing packages')
-
-
-def create_package_set_compat(yml, arch, flavor, setname):
- if setname == 'main':
- oldname = 'packages'
- elif setname == 'unpack':
- oldname = 'unpack_packages'
- else:
- return None
- if oldname not in yml:
- return PkgSet(setname) if setname == 'unpack' else None
- pkgset = PkgSet(setname)
- for entry in list(yml[oldname]):
- if type(entry) == dict:
- if 'flavors' in entry:
- if flavor is None or flavor not in entry['flavors']:
- continue
- if 'architectures' in entry:
- if arch not in entry['architectures']:
- continue
- pkgset.add_specs(entry['packages'])
- else:
- pkgset.add_specs([str(entry)])
- return pkgset
+ if missing_package and 'ignore_missing_packages' not in
yml['build_options']:
+ die('Abort due to missing meta packages')
def create_package_set_all(setname, pool, arch):
@@ -956,13 +1118,8 @@
return pkgset
-def create_package_set(yml, arch, flavor, setname, pool=None):
- if 'packagesets' not in yml:
- pkgset = create_package_set_compat(yml, arch, flavor, setname)
- if pkgset is None:
- die(f'package set {setname} is not defined')
- return pkgset
+def create_package_set(yml, arch, flavor, setname, pool=None):
pkgsets = {}
for entry in list(yml['packagesets']):
name = entry['name'] if 'name' in entry else 'main'
@@ -992,7 +1149,7 @@
if oname == name or oname not in pkgsets:
die(f'package set {oname} does not exist')
if pkgsets[oname] is None:
- pkgsets[oname] = PkgSet(oname) # instantiate
+ pkgsets[oname] = PkgSet(oname) # instantiate
if setop == 'add':
pkgset.add(pkgsets[oname])
elif setop == 'sub':
@@ -1005,7 +1162,7 @@
if setname not in pkgsets:
die(f'package set {setname} is not defined')
if pkgsets[setname] is None:
- pkgsets[setname] = PkgSet(setname) # instantiate
+ pkgsets[setname] = PkgSet(setname) # instantiate
return pkgsets[setname]
@@ -1021,16 +1178,16 @@
if 'updateinfo_packages_only' in yml['build_options']:
if not pool.updateinfos:
die("filtering for updates enabled, but no updateinfo found")
+ if singlemode:
+ die("filtering for updates enabled, but
take_all_available_versions is not set")
referenced_update_rpms = {}
for u in sorted(pool.lookup_all_updateinfos()):
for update in u.root.findall('update'):
parent = update.findall('pkglist')[0].findall('collection')[0]
-
for pkgentry in parent.findall('package'):
referenced_update_rpms[pkgentry.get('src')] = 1
-
main_pkgset = create_package_set(yml, arch, flavor, 'main', pool=pool)
missing_package = None
@@ -1042,6 +1199,8 @@
rpms = pool.lookup_all_rpms(arch, sel.name, sel.op, sel.epoch,
sel.version, sel.release)
if not rpms:
+ if referenced_update_rpms is not None:
+ continue
warn(f"package {sel} not found for {arch}")
missing_package = True
continue
@@ -1049,7 +1208,7 @@
for rpm in rpms:
if referenced_update_rpms is not None:
if (rpm.arch + '/' + rpm.canonfilename) not in
referenced_update_rpms:
- print(f"No update for {rpm}")
+ note(f"No update for {rpm}")
continue
link_entry_into_dir(rpm, rpmdir, add_slsa=add_slsa)
@@ -1082,7 +1241,7 @@
if drpm:
link_entry_into_dir(drpm, debugdir, add_slsa=add_slsa)
- if missing_package and not 'ignore_missing_packages' in
yml['build_options']:
+ if missing_package and 'ignore_missing_packages' not in
yml['build_options']:
die('Abort due to missing packages')
@@ -1128,7 +1287,16 @@
continue
binary = ET.SubElement(root, 'binary')
binary.text = 'obs://' + entry.origin
- for tag in 'name', 'epoch', 'version', 'release', 'arch', 'buildtime',
'disturl', 'license':
+ for tag in (
+ 'name',
+ 'epoch',
+ 'version',
+ 'release',
+ 'arch',
+ 'buildtime',
+ 'disturl',
+ 'license',
+ ):
val = getattr(entry, tag, None)
if val is None or val == '':
continue
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/product-composer-0.5.14/src/productcomposer/core/PkgSet.py
new/product-composer/src/productcomposer/core/PkgSet.py
--- old/product-composer-0.5.14/src/productcomposer/core/PkgSet.py
2025-05-08 12:37:30.000000000 +0200
+++ new/product-composer/src/productcomposer/core/PkgSet.py 2025-05-16
15:31:50.000000000 +0200
@@ -51,11 +51,11 @@
if name not in otherbyname:
pkgs.append(sel)
continue
- for osel in otherbyname[name]:
+ for other_sel in otherbyname[name]:
if sel is not None:
- sel = sel.sub(osel)
+ sel = sel.sub(other_sel)
if sel is not None:
- pkgs.append(p)
+ pkgs.append(sel)
self.pkgs = pkgs
self.byname = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/product-composer-0.5.14/src/productcomposer/core/Pool.py
new/product-composer/src/productcomposer/core/Pool.py
--- old/product-composer-0.5.14/src/productcomposer/core/Pool.py
2025-05-08 12:37:30.000000000 +0200
+++ new/product-composer/src/productcomposer/core/Pool.py 2025-05-16
15:31:50.000000000 +0200
@@ -57,7 +57,7 @@
def lookup_all_updateinfos(self):
return self.updateinfos.values()
- def remove_rpms(self, arch, name, epoch=None, version=None, release=None):
+ def remove_rpms(self, arch, name, op=None, epoch=None, version=None,
release=None):
if name not in self.rpms:
return
self.rpms[name] = [rpm for rpm in self.rpms[name] if not
rpm.matches(arch, name, op, epoch, version, release)]