diff --git a/man/paru.conf.5 b/man/paru.conf.5 index 72b4507d..e61c59d7 100644 --- a/man/paru.conf.5 +++ b/man/paru.conf.5 @@ -8,7 +8,7 @@ paru.conf \- paru configuration file $PARU_CONF, $XDG_CONFIG_HOME/paru/paru.conf, $HOME/.config/paru/paru.conf, /etc/paru.conf .SH DESCRIPTION -Paru's config file. Based on the format used by +Paru's config file. Based on the format used by .BR pacman.conf (5) Paru first attempts to read the file at $PARU_CONF. If $PARU_CONF is not @@ -178,7 +178,7 @@ during builds allowing an option to be chosen then. .TP .B UpgradeMenu Show a detailed list of updates in a similar format to pacman's VerbosePkgLists -option. (See +option. (See .BR pacman.conf(5)). Upgrades can be skipped using numbers, number ranges, or repo names. @@ -208,7 +208,7 @@ visible here: https://aur.archlinux.org/packages/ .TP .B SearchBy = -Defaults to name-desc. Search AUR packages according to the options in +Defaults to name-desc. Search AUR packages according to the options in "Search by" visible here: https://aur.archlinux.org/packages/ .TP @@ -466,6 +466,12 @@ MakepkgConf option for this package only. Set environment variables for building this package. Values may optionally be quoted with double quotes. Multiple key-value pairs are separated by commas. +.TP +.B PkgbuildPatches = { https://url-patch.patch, /local/patch.patch } +List patches for the PKGBUILD to apply to the PKGBUILD before building this +package. You may for example add patches to the source of PKGBUILD by adding them +locally then generating a PKGBUILD patch. + .SS GROUP OVERRIDES A group override section applies the same settings to multiple packages: diff --git a/src/config.rs b/src/config.rs index 3da91ad4..302cfcb2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use crate::exec::{self, Status}; use crate::fmt::color_repo; use crate::info::get_terminal_width; use crate::pkgbuild::PkgbuildRepos; -use crate::util::{get_provider, reopen_stdin}; +use crate::util::{get_provider, reopen_stdin, split_by_comma}; use crate::{alpm_debug_enabled, help, printtr, repo}; use std::collections::HashMap; @@ -14,6 +14,7 @@ use std::fmt; use std::fs::{remove_file, OpenOptions}; use std::io::{stderr, stdin, stdout, BufRead, IsTerminal}; use std::path::{Path, PathBuf}; +use std::process::Command; use std::str::FromStr; use alpm::{ @@ -26,6 +27,7 @@ use anyhow::{anyhow, bail, ensure, Context, Error, Result}; use bitflags::bitflags; use cini::{Callback, CallbackKind, Ini}; use globset::{Glob, GlobSet, GlobSetBuilder}; +use reqwest::get; use tr::tr; use url::Url; @@ -381,10 +383,90 @@ impl ConfigEnum for YesNoAllTree { ]; } +#[derive(Debug, Clone)] +pub enum PatchSource { + Path(PathBuf), + Url(Url), +} + +impl PatchSource { + pub async fn apply(&self, config: &Config, dir: &Path) -> Result<()> { + match self { + PatchSource::Path(path) => { + let patch_path = if path.is_absolute() { + path.clone() + } else { + dir.join(path) + }; + + let mut cmd = Command::new(&config.git_bin); + cmd.arg("apply").arg("-v").arg(&patch_path).current_dir(dir); + + exec::command(&mut cmd) + .with_context(|| tr!("failed to apply patch '{}'", patch_path.display()))?; + } + PatchSource::Url(url) => { + let bytes = get(url.clone()) + .await + .with_context(|| tr!("Failed to download patch from {}", url))? + .error_for_status() + .with_context(|| tr!("Failed to download patch from {}", url))? + .bytes() + .await?; + + // tempfile for patch. + let mut temp = tempfile::NamedTempFile::new()?; + use std::io::Write; + temp.write_all(&bytes)?; + + let mut cmd = Command::new(&config.git_bin); + cmd.arg("apply").arg("-v").arg(temp.path()).current_dir(dir); + + exec::command(&mut cmd) + .with_context(|| tr!("failed to apply patch '{}'", temp.path().display()))?; + } // tempfile cleaned up safely by drop. + } + + Ok(()) + } +} + +impl FromStr for PatchSource { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let Ok(url) = Url::parse(s) { + Ok(PatchSource::Url(url)) + } else { + Ok(PatchSource::Path(PathBuf::from(s))) + } + } +} + #[derive(Debug, Default, Clone)] pub struct PackageOverride { pub makepkg_conf: Option, pub env: Vec<(String, String)>, + pub pkgbuild_patches: Vec, +} + +impl PackageOverride { + pub(crate) fn merge_from(&mut self, src: &PackageOverride) { + if src.makepkg_conf.is_some() { + self.makepkg_conf = src.makepkg_conf.clone(); + } + + for (k, v) in &src.env { + if let Some(existing) = self.env.iter_mut().find(|(ek, _)| ek == k) { + existing.1 = v.clone(); + } else { + self.env.push((k.clone(), v.clone())); + } + } + + self.pkgbuild_patches + .extend(src.pkgbuild_patches.iter().cloned()); + } } #[derive(Debug)] @@ -393,6 +475,7 @@ enum OverrideParseState { name: String, makepkg_conf: Option, env: Vec<(String, String)>, + pkgbuild_patches: Vec, }, Group { name: String, @@ -597,6 +680,7 @@ impl Ini for Config { name: rest.to_string(), makepkg_conf: None, env: Vec::new(), + pkgbuild_patches: Vec::new(), }); } else if let Some(rest) = section.strip_prefix("override.group.") { ensure!( @@ -1255,6 +1339,22 @@ then initialise it with: } } } + "PkgbuildPatches" => { + let value = value.context(tr!("value can not be empty for key '{}'", key))?; + let parsed = parse_patches_list(value)?; + match state { + OverrideParseState::Package { + pkgbuild_patches, .. + } => { + pkgbuild_patches.extend(parsed); + } + OverrideParseState::Group { .. } => { + bail!(tr!( + "'PkgbuildPatches' is not valid in [override.group.*] sections" + )); + } + } + } _ => eprintln!( "{}", tr!("error: unknown option '{}' in override section", key) @@ -1275,16 +1375,23 @@ then initialise it with: name, makepkg_conf, env, + pkgbuild_patches, } => { ensure!( - makepkg_conf.is_some() || !env.is_empty(), + makepkg_conf.is_some() || !env.is_empty() || !pkgbuild_patches.is_empty(), tr!( - "override.package.{} must have MakepkgConf or Overrides", + "override.package.{} must have MakepkgConf, Overrides or PkgbuildPatches", name ) ); - self.overrides - .insert(name, PackageOverride { makepkg_conf, env }); + self.overrides.insert( + name, + PackageOverride { + makepkg_conf, + env, + pkgbuild_patches, + }, + ); } OverrideParseState::Group { name, @@ -1302,7 +1409,6 @@ then initialise it with: ); for pkg in packages { let entry = self.overrides.entry(pkg).or_default(); - // Group overrides merge: later groups overwrite conflicting keys if let Some(ref conf) = makepkg_conf { entry.makepkg_conf = Some(conf.clone()); } @@ -1343,22 +1449,7 @@ fn parse_overrides_map(input: &str) -> Result> { } let mut result = Vec::new(); - let mut pairs = Vec::new(); - let mut current = String::new(); - let mut in_quotes = false; - for ch in inner.chars() { - if ch == '"' { - in_quotes = !in_quotes; - current.push(ch); - } else if ch == ',' && !in_quotes { - pairs.push(std::mem::take(&mut current)); - } else { - current.push(ch); - } - } - if !current.is_empty() { - pairs.push(current); - } + let pairs = split_by_comma(inner); for pair in &pairs { let pair = pair.trim(); @@ -1392,6 +1483,35 @@ fn parse_overrides_map(input: &str) -> Result> { Ok(result) } +fn parse_patches_list(input: &str) -> Result> { + let input = input.trim(); + let inner = input + .strip_prefix('{') + .and_then(|s| s.strip_suffix('}')) + .context(tr!("PkgbuildPatches value must be wrapped in { }"))? + .trim(); + + if inner.is_empty() { + return Ok(Vec::new()); + } + + let parts = split_by_comma(inner); + + let mut result = Vec::new(); + for part in parts { + let value = part + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .unwrap_or(part.as_str()) + .to_string(); + + ensure!(!value.is_empty(), tr!("Patch source can not be empty")); + result.push(value.parse()?); + } + + Ok(result) +} + pub fn version() { let ver = option_env!("PARU_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")); print!("paru v{}", ver); diff --git a/src/install.rs b/src/install.rs index bfba44bb..8f478e2d 100644 --- a/src/install.rs +++ b/src/install.rs @@ -13,7 +13,9 @@ use crate::args::{Arg, Args}; use crate::chroot::Chroot; use crate::clean::clean_untracked; use crate::completion::update_aur_cache; -use crate::config::{Config, LocalRepos, Mode, Op, PackageOverride, Sign, YesNoAllTree, YesNoAsk}; +use crate::config::{ + Config, LocalRepos, Mode, Op, PackageOverride, PatchSource, Sign, YesNoAllTree, YesNoAsk, +}; use crate::devel::{fetch_devel_info, load_devel_info, save_devel_info, DevelInfo}; use crate::download::{self, Bases}; use crate::exec::{command_status, has_command}; @@ -528,12 +530,13 @@ impl Installer { } // TODO: sort out args - fn build_pkgbuild( + async fn build_pkgbuild( &mut self, config: &mut Config, base: &mut Base, repo: Option<(&str, &str)>, dir: &Path, + cwd: &Path, ) -> Result<(HashMap, String)> { let pkgdest = repo.map(|r| r.1); @@ -549,6 +552,8 @@ impl Installer { self.chroot.makepkg_conf = conf.clone(); } } + + apply_patches(config, &cwd, &ov.pkgbuild_patches).await?; } let result = self.build_pkgbuild_inner(config, base, repo, dir, pkgdest, &pkg_override); @@ -751,11 +756,12 @@ impl Installer { Ok(()) } - fn build_install_pkgbuild( + async fn build_install_pkgbuild( &mut self, config: &mut Config, base: &mut Base, repo: Option<(&str, &str)>, + cwd: &Path, ) -> Result<()> { let dir = match base { Base::Aur(_) => config.build_dir.join(base.package_base()), @@ -833,7 +839,7 @@ impl Installer { } let (mut pkgdest, version) = if build { - self.build_pkgbuild(config, base, repo, &dir)? + self.build_pkgbuild(config, base, repo, &dir, &cwd).await? } else { printtr!("{}: parsing pkg list...", base); let (pkgdests, version) = parse_package_list(config, &dir, pkgdest)?; @@ -895,6 +901,7 @@ impl Installer { &mut self, config: &mut Config, build: &mut [Base], + cwd: &Path, ) -> Result<()> { if config.devel { printtr!("fetching devel info..."); @@ -927,9 +934,9 @@ impl Installer { .as_ref() .map(|(name, file)| (name.as_str(), file.as_str())); - let err = self.build_install_pkgbuild(config, base, repo_server); + let err = self.build_install_pkgbuild(config, base, repo_server, &cwd); - match err { + match err.await { Ok(_) => { self.failed.pop().unwrap(); } @@ -994,8 +1001,8 @@ impl Installer { config.pkgbuild_repos.refresh(config)?; self.done_something = true; } - self.resolve_targets(config, &repo_targets, &aur_targets) - .await + let cwd = std::env::current_dir()?; + self.resolve_targets(config, &repo_targets, &aur_targets, &cwd).await } async fn resolve_targets<'a>( @@ -1003,6 +1010,7 @@ impl Installer { config: &mut Config, repo_targets: &[Targ<'a>], aur_targets: &[Targ<'a>], + cwd: &Path, ) -> Result<()> { let mut cache = Cache::new(); let flags = flags(config); @@ -1039,7 +1047,7 @@ impl Installer { targets.extend(self.upgrades.repo_keep.iter().map(Targ::from)); - if self.shoud_just_pacman(config.mode, aur_targets, &self.upgrades, self.ran_pacman) { + if self.should_just_pacman(config.mode, aur_targets, &self.upgrades, self.ran_pacman) { print_warnings(config, &cache, None); let mut args = config.pacman_args(); let targets = targets.iter().map(|t| t.to_string()).collect::>(); @@ -1086,7 +1094,7 @@ impl Installer { let mut err = Ok(()); if !build.is_empty() { - err = self.build_install_pkgbuilds(config, &mut build).await; + err = self.build_install_pkgbuilds(config, &mut build, &cwd).await; } if err.is_ok() && config.chroot { @@ -1102,7 +1110,7 @@ impl Installer { err } - fn shoud_just_pacman( + fn should_just_pacman( &self, mode: Mode, aur_targets: &[Targ<'_>], @@ -1307,6 +1315,13 @@ impl Installer { } } +async fn apply_patches(config: &Config, dir: &Path, patches: &[PatchSource]) -> Result<()> { + for patch in patches { + patch.apply(config, dir).await?; + } + Ok(()) +} + fn get_base_override(config: &Config, base: &Base) -> Option { let mut result: Option = None; @@ -1314,18 +1329,7 @@ fn get_base_override(config: &Config, base: &Base) -> Option { if let Some(ov) = config.overrides.get(pkg_name) { match result { None => result = Some(ov.clone()), - Some(ref mut merged) => { - if ov.makepkg_conf.is_some() { - merged.makepkg_conf = ov.makepkg_conf.clone(); - } - for (k, v) in &ov.env { - if let Some(existing) = merged.env.iter_mut().find(|(ek, _)| ek == k) { - existing.1 = v.clone(); - } else { - merged.env.push((k.clone(), v.clone())); - } - } - } + Some(ref mut merged) => merged.merge_from(ov), } } } diff --git a/src/util.rs b/src/util.rs index d55df329..67a7a2a0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -400,3 +400,26 @@ pub fn is_arch_repo(name: &str) -> bool { | "multilib-testing" ) } + +pub fn split_by_comma(inner: &str) -> Vec { + let mut items = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + + for ch in inner.chars() { + if ch == '"' { + in_quotes = !in_quotes; + current.push(ch); + } else if ch == ',' && !in_quotes { + items.push(std::mem::take(&mut current)); + } else { + current.push(ch); + } + } + + if !current.is_empty() { + items.push(current); + } + + items +} \ No newline at end of file diff --git a/testdata/overrides/firefox-comment-addition.patch b/testdata/overrides/firefox-comment-addition.patch new file mode 100644 index 00000000..17b8dd05 --- /dev/null +++ b/testdata/overrides/firefox-comment-addition.patch @@ -0,0 +1,22 @@ +From 02db1a2f0f393209c4b8274de2755f12abe7859f Mon Sep 17 00:00:00 2001 +From: Toria +Date: Sat, 18 Apr 2026 20:11:43 +0100 +Subject: [PATCH] Test comment, PKGBUILD for url test. + +Signed-off-by: Toria +--- + PKGBUILD | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/PKGBUILD b/PKGBUILD +index 04f2e7d..a634854 100644 +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -1,3 +1,4 @@ ++# This is for a test! + # Maintainer: Jan Alexander Steffens (heftig) + # Contributor: Ionut Biru + # Contributor: Jakub Schmidtke +-- +2.53.0 + diff --git a/testdata/overrides/firefox-pkgrel-bump.patch b/testdata/overrides/firefox-pkgrel-bump.patch new file mode 100644 index 00000000..5d74ce24 --- /dev/null +++ b/testdata/overrides/firefox-pkgrel-bump.patch @@ -0,0 +1,25 @@ +From cc955c6b5a182f8d0f0cd7dfa1cdc44bf5b0d11d Mon Sep 17 00:00:00 2001 +From: Toria +Date: Sat, 18 Apr 2026 20:04:10 +0100 +Subject: [PATCH] PKGREL bump for paru test. + +Signed-off-by: Toria +--- + PKGBUILD | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/PKGBUILD b/PKGBUILD +index 04f2e7d..ae4b0ee 100644 +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -4,7 +4,7 @@ + + pkgname=firefox + pkgver=149.0.2 +-pkgrel=1 ++pkgrel=2 + pkgdesc="Fast, Private & Safe Web Browser" + url="https://www.mozilla.org/firefox/" + arch=(x86_64) +-- +2.53.0 diff --git a/testdata/paru.conf b/testdata/paru.conf index 7bf27414..764f67da 100644 --- a/testdata/paru.conf +++ b/testdata/paru.conf @@ -18,6 +18,7 @@ Path = testdata/pkgbuild-repo [override.package.firefox] MakepkgConf = /etc/makepkg-pgo.conf Overrides = { CFLAGS = "-O3", MAKEFLAGS = "-j8" } +PkgbuildPatches = { "overrides/firefox-pkgrel-bump.patch", "https://raw.githubusercontent.com/ninetailedtori/paru/refs/heads/pkgbuild-patches/testdata/overrides/firefox-comment-addition.patch" } [override.group.lto-builds] Packages = mesa vulkan-radeon