diff --git a/build-individual.nu b/build-individual.nu index 1d20a74..3d53f3f 100644 --- a/build-individual.nu +++ b/build-individual.nu @@ -7,7 +7,7 @@ let images = ls modules | each { |moduleDir| cd $moduleDir.name # module is unversioned - if ($"($moduleDir.name | path basename).sh" | path exists) { + if (glob $"($moduleDir.name | path basename).{sh,nu}" | any { path exists }) { print $"(ansi cyan)Found(ansi reset) (ansi cyan_bold)unversioned(ansi reset) (ansi cyan)module:(ansi reset) ($moduleDir.name | path basename)" diff --git a/build-unified.nu b/build-unified.nu index baccabe..3faddd0 100644 --- a/build-unified.nu +++ b/build-unified.nu @@ -9,7 +9,7 @@ mkdir ./modules-latest ls modules | each { |moduleDir| # module is unversioned - if ($"($moduleDir.name)/($moduleDir.name | path basename).sh" | path exists) { + if (glob $"($moduleDir.name)/($moduleDir.name | path basename).{sh,nu}" | any { path exists }) { print $"(ansi cyan)Found(ansi reset) (ansi cyan_bold)unversioned(ansi reset) (ansi cyan)module:(ansi reset) ($moduleDir.name | path basename)" @@ -56,4 +56,4 @@ let digest = ( print $"(ansi cyan)Signing image:(ansi reset) ($env.REGISTRY)/modules@($digest)" cosign sign -y --key env://COSIGN_PRIVATE_KEY $"($env.REGISTRY)/modules@($digest)" -print $"(ansi green_bold)DONE!(ansi reset)" \ No newline at end of file +print $"(ansi green_bold)DONE!(ansi reset)" diff --git a/modules.json b/modules.json index 66afaa8..b597ad2 100644 --- a/modules.json +++ b/modules.json @@ -10,6 +10,7 @@ "https://raw.githubusercontent.com/blue-build/modules/main/modules/gschema-overrides/module.yml", "https://raw.githubusercontent.com/blue-build/modules/main/modules/justfiles/module.yml", "https://raw.githubusercontent.com/blue-build/modules/main/modules/rpm-ostree/module.yml", + "https://raw.githubusercontent.com/blue-build/modules/main/modules/dnf/module.yml", "https://raw.githubusercontent.com/blue-build/modules/main/modules/initramfs/module.yml", "https://raw.githubusercontent.com/blue-build/modules/main/modules/script/module.yml", "https://raw.githubusercontent.com/blue-build/modules/main/modules/signing/module.yml", diff --git a/modules/dnf/README.md b/modules/dnf/README.md new file mode 100644 index 0000000..ad353a4 --- /dev/null +++ b/modules/dnf/README.md @@ -0,0 +1,70 @@ +# `dnf` + +The [`dnf`](https://docs.fedoraproject.org/en-US/quick-docs/dnf/) module offers pseudo-declarative package and repository management using `dnf`. + +The module first downloads the repository files from URLs declared under `repos:` into `/etc/yum.repos.d/`. The magic string `%OS_VERSION%` can be substituted with the current VERSION_ID (major Fedora version), which can be used, for example, for pulling correct versions of repositories which have fixed Fedora version in the URL. + +You can also add repository files directly into your git repository if URLs are not provided. For example: +```yml +repos: + - my-repository.repo # copies in .repo file from files/dnf/my-repository.repo to /etc/yum.repos.d/ +``` + +Specific COPR repositories can also be specified in `user/project` format in `copr:` array. + +If you use a repo that requires adding custom keys (eg. Brave Browser), you can import the keys by declaring the key URLs under `keys:`. The magic string acts the same as it does in `repos`. + +Then the module installs the packages declared under `install:` using `dnf -y install --refresh`, it removes the packages declared under `remove:` using `dnf -y remove`. If there are packages declared under both `install:` and `remove:` then removal is performed 1st & install 2nd. + +Installing RPM packages directly from a `http(s)` url that points to the RPM file is also supported, you can just put the URLs under `install:` and they'll be installed along with the other packages. The magic string `%OS_VERSION%` is substituted with the current VERSION_ID (major Fedora version) like with the `repos:` property. + +If an RPM is not available in a repository or as an URL, you can also install it directly from a file in your git repository. For example: +```yml +install: + - weird-package.rpm # tries to install files/dnf/weird-package.rpm +``` + +Additionally, the `dnf` module supports a fix for packages that install into `/opt/`. Installation for packages that install into folder names declared under `optfix:` are fixed using some symlinks. Directory path in `/opt/` for those packages should be provided in recipe, like in Example Configuration. + +There is an option to install & remove RPM groups if desired in `group-install:` & `group-remove:`. RPM groups removal & installation always run before packages removal & installation. To see the list of all available RPM groups, you can use `dnf group list` command. + +The module can also replace base RPM packages with packages from any repo. Under `replace:`, the module finds every pair of keys `- from-repo:` and `packages:`. (Multiple pairs are supported.) The module uses `- from-repo:` key to gather the repo for package replacement, then it replaces packages declared under `packages:` using the command `dnf -y distro-sync --refresh --repo "${repo}" "${packages}"`. The magic string `%OS_VERSION%` is substituted with the current VERSION_ID (major Fedora version) as already said above. You need to assure that you provided the repo in `repos:` before using replacement functionality. To gather the repo ID that you need to input, you can use `dnf repo list` command. + +:::note +[Removed packages are still present in the underlying ostree repository](https://coreos.github.io/rpm-ostree/administrator-handbook/#removing-a-base-package), what `remove` does is kind of like hiding them from the system, it doesn't free up storage space. +::: + +## `dnf` behavior options + +There are several options that can be enabled during the package/group install + removal & during package replace, which modify the behavior of the package manager during those operations. + +Those include: + +Install operation: + - `install-weak-dependencies` (`--setopt=install_weak_deps=True/False` flag) + - `skip-unavailable-packages` (`--skip-unavailable` flag) + - `skip-broken-packages` (`--skip-broken` flag) + - `allow-erasing-packages` (`--allowerasing` flag) + +Remove operation: + - `remove-unused-dependencies` (`--no-autoremove` flag) + + +### `dnf` install/replace behavior options + +#### `install-weak-dependencies` +`install-weak-dependencies` option is used to enable or disable installation of weak dependencies for every install & replace operation. By default, this option is true, which means that weak dependencies are installed by default. Which kind of dependencies are considered weak can be seen [here](https://docs.fedoraproject.org/en-US/packaging-guidelines/WeakDependencies/). + +#### `skip-unavailable-packages` +`skip-unavailable-packages` option is used to continue or abort install/replace operation if there are no packages available in the repo in install operation, or if they are not available in the system in replace operation. By default, this option is false, which means that install/replace operation aborts in case of unavailable packages. + +#### `skip-broken-packages` +`skip-broken-packages` option is used to continue or abort install/replace operation if there are broken packages in the system. By default, this option is false, which means that install/replace operation aborts in case of broken packages. + +#### `allow-erasing-packages` +`allow-erasing-packages` option is used to allow erasing/removing problematic packages if they cause issues in install/replace operation. By default, this option is false, which means that problematic packages won't be removed & operation will be aborted. + +### `dnf` package (non-group) removal behavior options + +#### `remove-unused-dependencies` +`remove-unused-dependencies` option is used to control the behavior of removing unused dependencies when some main packages are removed. By default, this option is true. Only compatible with removing packages, not compatible with removing RPM groups. \ No newline at end of file diff --git a/modules/dnf/bluebuild-optfix.service b/modules/dnf/bluebuild-optfix.service new file mode 100644 index 0000000..6470559 --- /dev/null +++ b/modules/dnf/bluebuild-optfix.service @@ -0,0 +1,11 @@ +[Unit] +Description=Create symbolic links for directories in /usr/lib/opt/ to /var/opt/ +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/usr/libexec/bluebuild/optfix.sh +RemainAfterExit=no + +[Install] +WantedBy=default.target diff --git a/modules/dnf/dnf.nu b/modules/dnf/dnf.nu new file mode 100644 index 0000000..8b5c20b --- /dev/null +++ b/modules/dnf/dnf.nu @@ -0,0 +1,553 @@ +#!/usr/bin/env nu + +def repos [$repos: record]: nothing -> record { + let repos = $repos + | default [] keys + + let cleanup_repos = match $repos.files? { + [..$files] => { + add_repos $files + } + { add: [..$add] } => { + add_repos $add + } + { remove: [..$remove] } => { + remove_repos $remove + [] + } + { + add: [..$add] + remove: [..$remove] + } => { + let repos = add_repos $add + remove_repos $remove + $repos + } + } + + let cleanup_coprs = match $repos.copr? { + [..$coprs] => { + add_coprs $coprs + } + { enable: [..$enable] } => { + add_coprs $enable + } + { disable: [..$disable] } => { + disable_coprs $disable + [] + } + { + enable: [..$enable] + disable: [..$disable] + } => { + let coprs = add_coprs $enable + disable_coprs $disable + $coprs + } + } + + add_keys $repos.keys + + { + copr: { + disable: $cleanup_coprs + } + files: { + remove: $cleanup_repos + } + } +} + +def add_repos [$repos: list]: nothing -> list { + if ($repos | is-not-empty) { + print $'(ansi green)Adding repositories:(ansi reset)' + + # Substitute %OS_VERSION% & remove newlines/whitespaces from all repo entries + let repos = $repos + | each { + str replace --all '%OS_VERSION%' $env.OS_VERSION + | str trim + } + $repos + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + for $repo in $repos { + let repo = if ($repo | str starts-with 'https://') or ($repo | str starts-with 'http://') { + print $"Adding repository URL: (ansi cyan)'($repo)'(ansi reset)" + $repo + } else if ($repo | str ends-with '.repo') and ($'($env.CONFIG_DIRECTORY)/dnf/($repo)' | path exists) { + print $"Adding repository file: (ansi cyan)'($repo)'(ansi reset)" + $env.CONFIG_DIRECTORY | path join dnf $repo + } else { + return (error make { + msg: $"(ansi red)Urecognized repo (ansi cyan)'($repo)'(ansi reset)" + label: { + span: (metadata $repo).span + text: 'Found in config' + } + }) + } + + try { + ^dnf -y config-manager addrepo --from-repofile $repo + } + } + } + + let repo_files = $repos + | each {|repo| + path join etc yum.repos.d ($repo | path basename) + } + let repo_info = try { ^dnf5 repo list --json } + | from json + | select id + | get id + | par-each {|repo| + try { ^dnf5 repo info --json $repo } + | from json + } + | flatten + + $repo_info + | filter {|repo| + $repo.repo_file_path in $repo_files + } + | get id +} + +def remove_repos [$repos: list]: nothing -> nothing { + if ($repos | is-not-empty) { + print $'(ansi green)Removing repositories:(ansi reset)' + let repos = $repos + | each { + str trim + } + $repos + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + for $repo in $repos { + let repo = try { + ^dnf repo info --json $repo | from json + } + + for $file in $repo.repo_file_path { + print $'Removing file: (ansi cyan)($file)(ansi reset)' + rm -f $file + } + } + } +} + +def add_coprs [$copr_repos: list]: nothing -> list { + if ($copr_repos | is-not-empty) { + print $'(ansi green)Adding COPR repositories:(ansi reset)' + $copr_repos + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + for $copr in $copr_repos { + let is_copr = ($copr | split row / | length) == 2 + + if not $is_copr { + return (error make { + msg: $"(ansi red)The string '(ansi cyan)($copr)(ansi red)' is not recognized as a COPR repo(ansi reset)" + label: { + span: (metadata $is_copr).span + text: 'Checks if string is a COPR repo' + } + }) + } + + print $"Adding COPR repository: (ansi cyan)'($copr)'(ansi reset)" + try { + ^dnf -y copr enable $copr + } + } + } + $copr_repos +} + +def disable_coprs [$copr_repos: list]: nothing -> nothing { + if ($copr_repos | is-not-empty) { + print $'(ansi green)Adding COPR repositories:(ansi reset)' + $copr_repos + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + for $copr in $copr_repos { + let is_copr = ($copr | split row / | length) == 2 + + if not $is_copr { + return (error make { + msg: $"(ansi red)The string '(ansi cyan)($copr)(ansi red)' is not recognized as a COPR repo(ansi reset)" + label: { + span: (metadata $is_copr).span + text: 'Checks if string is a COPR repo' + } + }) + } + + print $"Disabling COPR repository: (ansi cyan)'($copr)'(ansi reset)" + try { + ^dnf -y copr disable $copr + } + } + } +} + +def add_keys [$keys: list]: nothing -> nothing { + if ($keys | is-not-empty) { + print $'(ansi green)Adding keys:(ansi reset)' + $keys + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + for $key in $keys { + let key = $key + | str replace --all '%OS_VERSION%' $env.OS_VERSION + | str trim + + try { + ^rpm --import $key + } + } + } +} + +def run_optfix [$optfix_pkgs: list]: nothing -> nothing { + const LIB_EXEC_DIR = '/usr/libexec/bluebuild' + const SYSTEMD_DIR = '/etc/systemd/system' + const MODULE_DIR = '/tmp/modules/dnf' + const LIB_OPT_DIR = '/usr/lib/opt' + const VAR_OPT_DIR = '/var/opt' + const OPTFIX_SCRIPT = 'optfix.sh' + const SERV_UNIT = 'bluebuild-optfix.service' + + if ($optfix_pkgs | is-not-empty) { + if not ($LIB_EXEC_DIR | path join $OPTFIX_SCRIPT | path exists) { + mkdir $LIB_EXEC_DIR + cp ($MODULE_DIR | path join $OPTFIX_SCRIPT) $'($LIB_EXEC_DIR)/' + + try { + chmod +x $'($LIB_EXEC_DIR | path join $OPTFIX_SCRIPT)' + } + } + + if not ($SYSTEMD_DIR | path join $SERV_UNIT | path exists) { + cp ($MODULE_DIR | path join $SERV_UNIT) $'($SYSTEMD_DIR)/' + + try { + ^systemctl enable $SERV_UNIT + } + } + + print $"(ansi green)Creating symlinks to fix packages that install to /opt:(ansi reset)" + $optfix_pkgs + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + mkdir $VAR_OPT_DIR + try { + ^ln -snf $VAR_OPT_DIR /opt + } + + for $opt in $optfix_pkgs { + let lib_dir = $LIB_OPT_DIR | path join $opt + mkdir $lib_dir + + try { + ^ln -sf $lib_dir ($VAR_OPT_DIR | path join $opt) + } + + print $"Created symlinks for '(ansi cyan)($opt)(ansi reset)'" + } + } +} + +def group_remove [remove: record]: nothing -> nothing { + let remove_list = $remove + | default [] packages + | get packages + + if ($remove_list | is-not-empty) { + print $'(ansi green)Removing group packages:(ansi reset)' + $remove_list + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + try { + ^dnf -y group remove ...($remove_list) + } + } +} + +def group_install [install: record]: nothing -> nothing { + let install = $install + | default true install-weak-dependencies + | default false skip-unavailable-packages + | default false skip-broken-packages + | default false allow-erasing-packages + | default [] packages + let install_list = $install + | get packages + | each { str trim } + + if ($install_list | is-not-empty) { + print $'(ansi cyan)Installing group packages:(ansi reset)' + $install_list + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + mut args = [] + + let weak_arg = if $install.install-weak-dependencies { + '--setopt=install_weak_deps=True' + } else { + '--setopt=install_weak_deps=False' + } + + if $install.skip-unavailable-packages { + $args = $args | append '--skip-unavailable' + } + + if $install.skip-broken-packages { + $args = $args | append '--skip-broken' + } + + if $install.allow-erasing-packages { + $args = $args | append '--allowerasing' + } + + try { + ^dnf -y $weak_arg group install --refresh ...($args) ...($install_list) + } + } +} + +def remove_pkgs [remove: record]: nothing -> nothing { + let remove = $remove + | default [] packages + | default true remove-unused-dependencies + + if ($remove.packages | is-not-empty) { + print $'(ansi green)Removing packages:(ansi reset)' + $remove.packages + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + mut args = [] + + if not $remove.remove-unused-dependencies { + $args = $args | append '--no-autoremove' + } + + try { + ^dnf -y remove ...($args) ...($remove.packages) + } + } +} + +def install_pkgs [install: record]: nothing -> nothing { + let install = $install + | default true install-weak-dependencies + | default false skip-unavailable-packages + | default false skip-broken-packages + | default false allow-erasing-packages + | default [] packages + + mut args = [] + + let weak_arg = if $install.install-weak-dependencies { + '--setopt=install_weak_deps=True' + } else { + '--setopt=install_weak_deps=False' + } + + if $install.skip-unavailable-packages { + $args = $args | append '--skip-unavailable' + } + + if $install.skip-broken-packages { + $args = $args | append '--skip-broken' + } + + if $install.allow-erasing-packages { + $args = $args | append '--allowerasing' + } + + let install_list = $install.packages + | filter {|pkg| ($pkg | describe) == 'string' } + | each { str replace --all '%OS_VERSION%' $env.OS_VERSION | str trim } + let http_list = $install_list + | filter {|pkg| + ($pkg | str starts-with 'https://') or ($pkg | str starts-with 'http://') + } + let local_list = $install_list + | filter {|pkg| + ($env.CONFIG_DIRECTORY | path join dnf $pkg | path exists) + } + let normal_list = $install_list + | filter {|pkg| + not ( + ($pkg | str starts-with 'https://') or ($pkg | str starts-with 'http://') + ) and not ( + ($env.CONFIG_DIRECTORY | path join dnf $pkg | path exists) + ) + } + + if ($install_list | is-not-empty) { + if ($http_list | is-not-empty) { + print $'(ansi green)Installing packages directly from URL:(ansi reset)' + $http_list + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + } + + if ($local_list | is-not-empty) { + print $'(ansi green)Installing local packages:(ansi reset)' + $local_list + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + } + + if ($normal_list | is-not-empty) { + print $'(ansi green)Installing packages:(ansi reset)' + $normal_list + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + } + + try { + ^dnf -y $weak_arg install --refresh ...($args) ...($install_list) + } + } + + let repo_install_list = $install.packages + | filter {|pkg| 'repo' in $pkg and 'packages' in $pkg } + + for $repo_install in $repo_install_list { + let repo = $repo_install.repo + let packages = $repo_install.packages + + print $'(ansi green)Installing packages for repo (ansi cyan)($repo)(ansi green):(ansi reset)' + $packages + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + try { + ^dnf -y $weak_arg install --refresh --repoid $repo ...($args) ...($packages) + } + } +} + +def replace_pkgs [replace_list: list]: nothing -> nothing { + if ($replace_list | is-not-empty) { + for $replacement in $replace_list { + let replacement = $replacement + | default [] packages + | default true install-weak-dependencies + | default false skip-unavailable-packages + | default false skip-broken-packages + | default false allow-erasing-packages + + if ($replacement.packages | is-not-empty) { + let has_from_repo = 'from-repo' in $replacement + + if not $has_from_repo { + return (error make { + msg: $"(ansi red)A value is expected in key 'from-repo'(ansi reset)" + label: { + span: (metadata $replacement).span + text: "Checks for 'from-repo' property" + } + }) + } + + let from_repo = $replacement + | get from-repo + + print $"(ansi green)Replacing packages from '(ansi cyan)($from_repo)(ansi green)':(ansi reset)" + $replacement.packages + | each { + print $'- (ansi cyan)($in)(ansi reset)' + } + + mut args = [] + + let weak_arg = if $replacement.install-weak-dependencies { + '--setopt=install_weak_deps=True' + } else { + '--setopt=install_weak_deps=False' + } + + if $replacement.skip-unavailable-packages { + $args = $args | append '--skip-unavailable' + } + + if $replacement.skip-broken-packages { + $args = $args | append '--skip-broken' + } + + if $replacement.allow-erasing-packages { + $args = $args | append '--allowerasing' + } + + try { + ^dnf -y $weak_arg distro-sync --refresh ...($args) --repo $from_repo ...($replacement.packages) + } + } + } + } +} + +def main [config: string]: nothing -> nothing { + let config = $config + | from json + | default {} repos + | default {} group-remove + | default {} group-install + | default {} remove + | default {} install + | default [] replace + let has_dnf5 = ^rpm -q dnf5 | complete + let should_cleanup = $config.repos + | default false cleanup + | get cleanup + + if $has_dnf5.exit_code != 0 { + return (error make { + msg: $"(ansi red)ERROR: Main dependency '(ansi cyan)dnf5(ansi red)' is not installed. Install '(ansi cyan)dnf5(ansi red)' before using this module to solve this error.(ansi reset)" + label: { + span: (metadata $has_dnf5).span + text: 'Checks for dnf5' + } + }) + } + + let cleanup_repos = repos $config.repos + group_remove $config.group-remove + group_install $config.group-install + remove_pkgs $config.remove + install_pkgs $config.install + replace_pkgs $config.replace + + if $should_cleanup { + print $'(ansi green)Cleaning up added repos(ansi reset)' + repos $cleanup_repos + } +} diff --git a/modules/dnf/dnf.tsp b/modules/dnf/dnf.tsp new file mode 100644 index 0000000..0f5de00 --- /dev/null +++ b/modules/dnf/dnf.tsp @@ -0,0 +1,150 @@ +import "@typespec/json-schema"; +using TypeSpec.JsonSchema; + +@jsonSchema("/modules/dnf-latest.json") +model DnfModuleLatest { + ...DnfModuleV1; +} + +@jsonSchema("/modules/dnf-v1.json") +model DnfModuleV1 { + /** + * The dnf module offers pseudo-declarative package and repository management using dnf. + * https://blue-build.org/reference/modules/dnf/ + */ + type: "dnf" | "dnf@v1" | "dnf@latest"; + + /** List of links to .repo files to download into /etc/yum.repos.d/. */ + repos?: DnfRepo; + + /** List of folder names under /opt/ to enable for installing into. */ + optfix?: Array; + + /** Configuration of RPM groups removal. */ + `group-remove`?: DnfGroupRemove; + + /** Configuration of RPM groups install. */ + `group-install`?: DnfGroupInstall; + + /** Configuration of RPM packages removal. */ + remove?: DnfRemove; + + /** Configuration of RPM packages install. */ + install?: DnfInstall; + + /** List of configurations for replacing packages from another repo. */ + replace?: Array; +} + +model DnfRepo { + /** Cleans up the repos added in the same step after packages are installed. */ + cleanup?: boolean = false; + + /** List of paths or URLs to .repo files to import */ + files?: Array | DnfRepoFiles; + + /** + * List of COPR project repos to add. + * You can also specify 2 lists + * instead to 'enable' or 'disable' COPR repos. + */ + copr?: Array | DnfRepoCopr; + + /** List of links to key files to import for installing from custom repositories. */ + keys?: Array; +} + +model DnfRepoFiles { + /** List of repo files/URLs to add. */ + add?: Array; + + /** + * List of repos to remove. + * This must be the ID of the repo + * as seen in `dnf5 repolist`. + */ + remove?: Array; +} + +model DnfRepoCopr { + /** List of COPR repos to enable */ + enable?: Array; + + /** List of COPR repos to disable */ + disable?: Array; +} + +model DnfInstall { + /** List of RPM packages to install. */ + packages: Array; + + /** Whether to install weak dependencies during the RPM package install or not. */ + `install-weak-dependencies`?: boolean = true; + + /** Whether to continue with the RPM package install if there are no packages available in the repository. */ + `skip-unavailable-packages`?: boolean = false; + + /** Whether to continue with the RPM package install if there are broken packages. */ + `skip-broken-packages`?: boolean = false; + + /** Whether to allow erasing (removal) of packages in case of dependency problems during the RPM package install. */ + `allow-erasing-packages`?: boolean = false; +} + +model DnfInstallRepo { + /** The repo to use when installing packages */ + repo: string; + + /** List of RPM packages to install. */ + packages: Array; +} + +model DnfRemove { + /** List of RPM packages to remove. */ + packages: Array; + + /** Whether to remove unused dependencies during removal operation. */ + `remove-unused-dependencies`?: boolean = true; +} + +model DnfReplace { + /** URL to the source COPR repo for the new packages. */ + `from-repo`: string; + + /** List of packages to replace using packages from the defined repo. */ + packages: Array; + + /** Whether to install weak dependencies during the replacement or not. */ + `install-weak-dependencies`?: boolean = true; + + /** Whether to continue with the replacement if there are no packages available on the system to replace. */ + `skip-unavailable-packages`?: boolean = false; + + /** Whether to continue with the replacement if there are broken packages in the system during the replacement. */ + `skip-broken-packages`?: boolean = false; + + /** Whether to allow erasing (removal) of packages in case of dependency problems during the replacement. */ + `allow-erasing-packages`?: boolean = false; +} + +model DnfGroupInstall { + /** List of RPM groups to install. */ + packages: Array; + + /** Whether to install weak dependencies during the RPM group install or not. */ + `install-weak-dependencies`?: boolean = true; + + /** Whether to continue with the RPM group install if there are no packages available in the repository. */ + `skip-unavailable-packages`?: boolean = false; + + /** Whether to continue with the RPM group install if there are broken packages. */ + `skip-broken-packages`?: boolean = false; + + /** Whether to allow erasing (removal) of packages in case of dependency problems during the RPM group install. */ + `allow-erasing-packages`?: boolean = false; +} + +model DnfGroupRemove { + /** List of RPM groups to remove. */ + packages: Array; +} diff --git a/modules/dnf/module.yml b/modules/dnf/module.yml new file mode 100644 index 0000000..8b0957e --- /dev/null +++ b/modules/dnf/module.yml @@ -0,0 +1,36 @@ +name: dnf +shortdesc: The dnf module offers pseudo-declarative package and repository management using dnf. +example: | + type: dnf + repos: + - https://brave-browser-rpm-release.s3.brave.com/brave-browser.repo + copr: + - atim/starship + - trixieua/mutter-patched + keys: + - https://brave-browser-rpm-release.s3.brave.com/brave-core.asc + optfix: + - Tabby # needed because tabby installs into /opt/Tabby/ + - brave.com + group-install: + packages: + - cosmic-desktop + - cosmic-desktop-apps # Installs Cosmic desktop environment + - window-managers + install: + packages: + - starship + - brave-browser + - https://github.com/Eugeny/tabby/releases/download/v1.0.209/tabby-1.0.209-linux-x64.rpm + install-weak-dependencies: false # doesn't install weak dependencies for those packages + remove: + packages: + - firefox + - firefox-langpacks + replace: + - from-repo: copr:copr.fedorainfracloud.org:trixieua:mutter-patched + packages: + - mutter + - mutter-common + - gdm + skip-unavailable-packages: true # replacement will proceed even if 'mutter' or 'gdm' is not installed in the system \ No newline at end of file diff --git a/modules/dnf/optfix.sh b/modules/dnf/optfix.sh new file mode 100644 index 0000000..fcec512 --- /dev/null +++ b/modules/dnf/optfix.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SOURCE_DIR="/usr/lib/opt/" +TARGET_DIR="/var/opt/" + +# Ensure the target directory exists +mkdir -p "$TARGET_DIR" + +# Loop through directories in the source directory +for dir in "$SOURCE_DIR"*/; do + if [ -d "$dir" ]; then + # Get the base name of the directory + dir_name=$(basename "$dir") + + # Check if the symlink already exists in the target directory + if [ -L "$TARGET_DIR/$dir_name" ]; then + echo "Symlink already exists for $dir_name, skipping." + continue + fi + + # Create the symlink + ln -s "$dir" "$TARGET_DIR/$dir_name" + echo "Created symlink for $dir_name" + fi +done diff --git a/modules/rpm-ostree/rpm-ostree.sh b/modules/rpm-ostree/rpm-ostree.sh index f5f1564..80e5552 100644 --- a/modules/rpm-ostree/rpm-ostree.sh +++ b/modules/rpm-ostree/rpm-ostree.sh @@ -60,7 +60,8 @@ if [[ ${#OPTFIX[@]} -gt 0 ]]; then echo "Creating symlinks to fix packages that install to /opt" # Create symlink for /opt to /var/opt since it is not created in the image yet mkdir -p "/var/opt" - ln -fs "/var/opt" "/opt" + ln -fs "/var/opt" "/opt" + # Create symlinks for each directory specified in recipe.yml for OPTPKG in "${OPTFIX[@]}"; do