Skip to content

Commit acbf1d6

Browse files
add initial support for replaces
1 parent 981d41e commit acbf1d6

6 files changed

Lines changed: 328 additions & 11 deletions

File tree

src/apk/compat.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ mod tests {
5656
name: name.to_string(),
5757
version: version.to_string(),
5858
depends: depends.into_iter().map(|s| s.to_string()).collect(),
59+
replaces: Vec::new(),
5960
}
6061
}
6162

src/apk/exec.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,14 @@ impl Apk {
128128
pub fn cache_purge(&self) -> Result<()> {
129129
self.run_silent(&["cache", "purge"])
130130
}
131+
132+
pub fn fetch(&self, packages: &[&str]) -> Result<()> {
133+
let mut args = vec!["fetch", "--cache"];
134+
args.extend(packages);
135+
self.run_silent(&args)
136+
}
137+
138+
pub fn cache_dir(&self) -> PathBuf {
139+
self.root.join("etc").join("apk").join("cache")
140+
}
131141
}

src/apk/index.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub struct Package {
1212
pub name: String,
1313
pub version: String,
1414
pub depends: Vec<String>,
15+
pub replaces: Vec<String>,
1516
}
1617

1718
impl Package {
@@ -88,6 +89,55 @@ pub fn find_best_compatible_version<'a>(
8889
compatible.first().copied()
8990
}
9091

92+
pub fn parse_apk_file(path: &str) -> Result<Package> {
93+
let f = File::open(path)?;
94+
let mut data = Vec::new();
95+
BufReader::new(f).read_to_end(&mut data)?;
96+
97+
let gz = MultiGzDecoder::new(Cursor::new(data));
98+
let mut archive = Archive::new(gz);
99+
100+
for entry in archive.entries()? {
101+
let mut entry = entry?;
102+
let entry_path = entry.path()?;
103+
104+
if entry_path.to_string_lossy() == ".PKGINFO" {
105+
let mut content = String::new();
106+
entry.read_to_string(&mut content)?;
107+
return parse_pkginfo(&content);
108+
}
109+
}
110+
111+
Err(anyhow!(".PKGINFO not found in package"))
112+
}
113+
114+
fn parse_pkginfo(content: &str) -> Result<Package> {
115+
let mut pkg = Package::default();
116+
117+
for line in content.lines() {
118+
let line = line.trim();
119+
if line.is_empty() || line.starts_with('#') {
120+
continue;
121+
}
122+
123+
if let Some((key, val)) = line.split_once(" = ") {
124+
match key {
125+
"pkgname" => pkg.name = val.to_string(),
126+
"pkgver" => pkg.version = val.to_string(),
127+
"depend" => pkg.depends.push(val.to_string()),
128+
"replaces" => pkg.replaces.push(val.to_string()),
129+
_ => {}
130+
}
131+
}
132+
}
133+
134+
if pkg.name.is_empty() {
135+
return Err(anyhow!("pkgname not found in .PKGINFO"));
136+
}
137+
138+
Ok(pkg)
139+
}
140+
91141
fn parse_index_from_tar_gz<R: Read>(reader: R) -> Result<Vec<Package>> {
92142
let mut data = Vec::new();
93143
let mut reader = reader;
@@ -140,6 +190,7 @@ fn parse_apkindex<R: BufRead>(reader: R) -> Result<Vec<Package>> {
140190
b'P' => current.name = val.to_string(),
141191
b'V' => current.version = val.to_string(),
142192
b'D' => current.depends = val.split_whitespace().map(|s| s.to_string()).collect(),
193+
b'r' => current.replaces = val.split_whitespace().map(|s| s.to_string()).collect(),
143194
_ => {}
144195
}
145196
}
@@ -161,6 +212,7 @@ mod tests {
161212
name: name.to_string(),
162213
version: version.to_string(),
163214
depends: depends.into_iter().map(|s| s.to_string()).collect(),
215+
replaces: Vec::new(),
164216
}
165217
}
166218

@@ -328,4 +380,25 @@ mod tests {
328380
assert_eq!(packages[0].version, "1.0.0");
329381
assert!(packages[0].depends.is_empty());
330382
}
383+
384+
#[test]
385+
fn parse_apkindex_replaces_field() {
386+
let input = "P:new-pkg\nV:1.0.0\nr:old-pkg-a old-pkg-b\n";
387+
let reader = BufReader::new(input.as_bytes());
388+
let packages = parse_apkindex(reader).unwrap();
389+
390+
assert_eq!(packages.len(), 1);
391+
assert_eq!(packages[0].name, "new-pkg");
392+
assert_eq!(packages[0].replaces, vec!["old-pkg-a", "old-pkg-b"]);
393+
}
394+
395+
#[test]
396+
fn parse_apkindex_replaces_empty() {
397+
let input = "P:test-pkg\nV:1.0.0\n";
398+
let reader = BufReader::new(input.as_bytes());
399+
let packages = parse_apkindex(reader).unwrap();
400+
401+
assert_eq!(packages.len(), 1);
402+
assert!(packages[0].replaces.is_empty());
403+
}
331404
}

src/apk/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ mod version;
66

77
pub use compat::check_os_compatibility;
88
pub use exec::Apk;
9-
pub use index::{fetch_remote_index, find_best_compatible_version, parse_index_tar_gz, Package};
9+
pub use index::{fetch_remote_index, find_best_compatible_version, parse_apk_file, parse_index_tar_gz, Package};
1010
pub use package::{generate_device_package, generate_remarkable_os_package};
1111
pub use version::version_lt;

src/commands/add.rs

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::fs;
22
use std::process;
33

4-
use crate::apk::{fetch_remote_index, find_best_compatible_version, parse_index_tar_gz, Apk, Package};
4+
use crate::apk::{
5+
fetch_remote_index, find_best_compatible_version, parse_apk_file, parse_index_tar_gz, Apk,
6+
Package,
7+
};
58
use crate::constants::VELLUM_ROOT;
69
use crate::device::get_apk_arch;
710

@@ -22,6 +25,7 @@ pub fn handle_add(apk: &Apk, args: &[String]) {
2225

2326
let mut resolved_args: Vec<String> = Vec::new();
2427
let mut resolved_packages: Vec<String> = Vec::new();
28+
let mut local_apk_packages: Vec<Package> = Vec::new();
2529
let mut has_incompatible = false;
2630

2731
for arg in args {
@@ -30,6 +34,14 @@ pub fn handle_add(apk: &Apk, args: &[String]) {
3034
continue;
3135
}
3236

37+
if arg.ends_with(".apk") {
38+
resolved_args.push(arg.clone());
39+
if let Ok(pkg) = parse_apk_file(arg) {
40+
local_apk_packages.push(pkg);
41+
}
42+
continue;
43+
}
44+
3345
match find_best_compatible_version(arg, &os_version, &index) {
3446
Some(pkg) => {
3547
resolved_args.push(format!("{}={}", pkg.name, pkg.version));
@@ -51,6 +63,34 @@ pub fn handle_add(apk: &Apk, args: &[String]) {
5163
process::exit(1);
5264
}
5365

66+
let fetch_args: Vec<&str> = resolved_args
67+
.iter()
68+
.filter(|a| !a.starts_with('-') && !a.ends_with(".apk"))
69+
.map(|s| s.as_str())
70+
.collect();
71+
if !fetch_args.is_empty() {
72+
let _ = apk.fetch(&fetch_args);
73+
}
74+
75+
let cached_packages = find_cached_apk_packages(apk, &resolved_args);
76+
let mut packages_to_replace = find_packages_to_replace(apk, &cached_packages);
77+
packages_to_replace.extend(find_packages_to_replace(apk, &local_apk_packages));
78+
if !packages_to_replace.is_empty() {
79+
println!("The following packages will be replaced:");
80+
for pkg in &packages_to_replace {
81+
println!(" - {pkg}");
82+
}
83+
println!();
84+
85+
let del_args: Vec<&str> = packages_to_replace.iter().map(|s| s.as_str()).collect();
86+
let mut del_cmd = vec!["del"];
87+
del_cmd.extend(del_args);
88+
if let Err(e) = apk.run(&del_cmd) {
89+
eprintln!("Failed to remove replaced packages: {e}");
90+
process::exit(1);
91+
}
92+
}
93+
5494
let mut cmd_args = vec!["add", "--cache-predownload"];
5595
cmd_args.extend(resolved_args.iter().map(|s| s.as_str()));
5696

@@ -148,3 +188,70 @@ fn clean_world_file_pins(packages: &[String]) {
148188

149189
let _ = fs::write(&world_path, new_content + "\n");
150190
}
191+
192+
fn find_packages_to_replace(apk: &Apk, cached_packages: &[Package]) -> Vec<String> {
193+
let installed = match apk.list_installed() {
194+
Ok(list) => list,
195+
Err(_) => return Vec::new(),
196+
};
197+
198+
let mut to_replace = Vec::new();
199+
200+
for pkg in cached_packages {
201+
for replaced in &pkg.replaces {
202+
if installed.contains(replaced) && !to_replace.contains(replaced) {
203+
to_replace.push(replaced.clone());
204+
}
205+
}
206+
}
207+
208+
to_replace
209+
}
210+
211+
fn find_cached_apk_packages(apk: &Apk, package_specs: &[String]) -> Vec<Package> {
212+
let cache_dir = apk.cache_dir();
213+
let entries = match fs::read_dir(&cache_dir) {
214+
Ok(e) => e,
215+
Err(_) => return Vec::new(),
216+
};
217+
218+
let apk_files: Vec<_> = entries
219+
.flatten()
220+
.filter_map(|entry| {
221+
let path = entry.path();
222+
let name = path.file_name()?.to_str()?;
223+
if name.ends_with(".apk") {
224+
Some(path)
225+
} else {
226+
None
227+
}
228+
})
229+
.collect();
230+
231+
let mut packages = Vec::new();
232+
233+
for spec in package_specs {
234+
let pkg_name = spec.split('=').next().unwrap_or(spec);
235+
236+
for apk_path in &apk_files {
237+
let file_name = match apk_path.file_name().and_then(|n| n.to_str()) {
238+
Some(n) => n,
239+
None => continue,
240+
};
241+
242+
if file_name.starts_with(&format!("{pkg_name}-")) {
243+
if let Some(path_str) = apk_path.to_str() {
244+
if let Ok(pkg) = parse_apk_file(path_str) {
245+
if pkg.name == pkg_name {
246+
packages.push(pkg);
247+
break;
248+
}
249+
}
250+
}
251+
}
252+
}
253+
}
254+
255+
packages
256+
}
257+

0 commit comments

Comments
 (0)