Skip to content

Commit c7e2197

Browse files
authored
feat: add reinstallation method to installer and transaction (#1128)
1 parent c765a53 commit c7e2197

File tree

5 files changed

+128
-56
lines changed

5 files changed

+128
-56
lines changed

crates/rattler-bin/src/commands/create.rs

+1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> {
288288
let transaction = Transaction::from_current_and_desired(
289289
installed_packages,
290290
required_packages,
291+
None,
291292
install_platform,
292293
)?;
293294

crates/rattler/src/install/installer/mod.rs

+22-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod error;
33
mod indicatif;
44
mod reporter;
55
use std::{
6-
collections::HashMap,
6+
collections::{HashMap, HashSet},
77
future::ready,
88
path::{Path, PathBuf},
99
sync::Arc,
@@ -20,7 +20,7 @@ use itertools::Itertools;
2020
use rattler_cache::package_cache::{CacheLock, CacheReporter};
2121
use rattler_conda_types::{
2222
prefix_record::{Link, LinkType},
23-
Platform, PrefixRecord, RepoDataRecord,
23+
PackageName, Platform, PrefixRecord, RepoDataRecord,
2424
};
2525
use rattler_networking::retry_policies::default_retry_policy;
2626
pub use reporter::Reporter;
@@ -50,6 +50,7 @@ pub struct Installer {
5050
target_platform: Option<Platform>,
5151
apple_code_sign_behavior: AppleCodeSignBehavior,
5252
alternative_target_prefix: Option<PathBuf>,
53+
reinstall_packages: Option<HashSet<PackageName>>,
5354
// TODO: Determine upfront if these are possible.
5455
// allow_symbolic_links: Option<bool>,
5556
// allow_hard_links: Option<bool>,
@@ -216,10 +217,27 @@ impl Installer {
216217
}
217218
}
218219

220+
/// Set the packages that we want explicitly to be reinstalled.
221+
#[must_use]
222+
pub fn with_reinstall_packages(self, reinstall: HashSet<PackageName>) -> Self {
223+
Self {
224+
reinstall_packages: Some(reinstall),
225+
..self
226+
}
227+
}
228+
229+
/// Set the packages that we want explicitly to be reinstalled.
230+
/// This function is similar to [`Self::with_reinstall_packages`],but
231+
/// modifies an existing instance.
232+
pub fn set_reinstall_packages(&mut self, reinstall: HashSet<PackageName>) -> &mut Self {
233+
self.reinstall_packages = Some(reinstall);
234+
self
235+
}
236+
219237
/// Sets the packages that are currently installed in the prefix. If this
220238
/// is not set, the installation process will first figure this out.
221239
///
222-
/// This function is similar to [`Self::set_installed_packages`],but
240+
/// This function is similar to [`Self::with_installed_packages`],but
223241
/// modifies an existing instance.
224242
pub fn set_installed_packages(&mut self, installed: Vec<PrefixRecord>) -> &mut Self {
225243
self.installed = Some(installed);
@@ -314,6 +332,7 @@ impl Installer {
314332
let transaction = Transaction::from_current_and_desired(
315333
installed,
316334
records.into_iter().collect::<Vec<_>>(),
335+
self.reinstall_packages,
317336
target_platform,
318337
)?;
319338

crates/rattler/src/install/test_utils.rs

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{
2+
path::{Path, PathBuf},
3+
str::FromStr,
4+
};
25

36
use futures::TryFutureExt;
4-
use rattler_conda_types::{PrefixRecord, RepoDataRecord};
7+
use rattler_conda_types::{Platform, PrefixRecord, RepoDataRecord, Version};
58
use rattler_networking::retry_policies::default_retry_policy;
69
use transaction::{Transaction, TransactionOperation};
10+
use url::Url;
711

812
use crate::{
13+
get_repodata_record,
914
install::{transaction, unlink_package, InstallDriver, InstallOptions},
1015
package_cache::PackageCache,
1116
};
1217

13-
use super::driver::PostProcessResult;
18+
use super::{driver::PostProcessResult, link_package, PythonInfo};
1419

1520
/// Install a package into the environment and write a `conda-meta` file that
1621
/// contains information about how the file was linked.
@@ -157,3 +162,42 @@ pub fn find_prefix_record<'a>(
157162
.iter()
158163
.find(|r| r.repodata_record.package_record.name.as_normalized() == name)
159164
}
165+
166+
pub async fn download_and_get_prefix_record(
167+
target_prefix: &Path,
168+
package_url: Url,
169+
sha256_hash: &str,
170+
) -> PrefixRecord {
171+
let package_path = tools::download_and_cache_file_async(package_url, sha256_hash)
172+
.await
173+
.unwrap();
174+
175+
let package_dir = tempfile::TempDir::new().unwrap();
176+
177+
// Create package cache
178+
rattler_package_streaming::fs::extract(&package_path, package_dir.path()).unwrap();
179+
180+
let py_info =
181+
PythonInfo::from_version(&Version::from_str("3.10").unwrap(), None, Platform::Linux64)
182+
.unwrap();
183+
let install_options = InstallOptions {
184+
python_info: Some(py_info),
185+
..InstallOptions::default()
186+
};
187+
188+
let install_driver = InstallDriver::default();
189+
// Link the package
190+
let paths = link_package(
191+
package_dir.path(),
192+
target_prefix,
193+
&install_driver,
194+
install_options,
195+
)
196+
.await
197+
.unwrap();
198+
199+
let repodata_record = get_repodata_record(&package_path);
200+
// Construct a PrefixRecord for the package
201+
202+
PrefixRecord::from_repodata_record(repodata_record, None, None, paths, None, None)
203+
}

crates/rattler/src/install/transaction.rs

+47-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::HashSet;
22

3-
use rattler_conda_types::{PackageRecord, Platform};
3+
use rattler_conda_types::{PackageName, PackageRecord, Platform};
44

55
use crate::install::{python::PythonInfoError, PythonInfo};
66

@@ -125,13 +125,15 @@ impl<Old: AsRef<New>, New> Transaction<Old, New> {
125125

126126
impl<Old: AsRef<PackageRecord>, New: AsRef<PackageRecord>> Transaction<Old, New> {
127127
/// Constructs a [`Transaction`] by taking the current situation and diffing
128-
/// that against the desired situation.
128+
/// that against the desired situation. You can specify a set of package names
129+
/// that should be reinstalled even if their content has not changed.
129130
pub fn from_current_and_desired<
130131
CurIter: IntoIterator<Item = Old>,
131132
NewIter: IntoIterator<Item = New>,
132133
>(
133134
current: CurIter,
134135
desired: NewIter,
136+
reinstall: Option<HashSet<PackageName>>,
135137
platform: Platform,
136138
) -> Result<Self, TransactionError>
137139
where
@@ -150,6 +152,7 @@ impl<Old: AsRef<PackageRecord>, New: AsRef<PackageRecord>> Transaction<Old, New>
150152
};
151153

152154
let mut operations = Vec::new();
155+
let reinstall = reinstall.unwrap_or_default();
153156

154157
let mut current_map = current_iter
155158
.clone()
@@ -179,7 +182,9 @@ impl<Old: AsRef<PackageRecord>, New: AsRef<PackageRecord>> Transaction<Old, New>
179182
let old_record = current_map.remove(name);
180183

181184
if let Some(old_record) = old_record {
182-
if !describe_same_content(record.as_ref(), old_record.as_ref()) {
185+
if !describe_same_content(record.as_ref(), old_record.as_ref())
186+
|| reinstall.contains(&record.as_ref().name)
187+
{
183188
// if the content changed, we need to reinstall (remove and install)
184189
operations.push(TransactionOperation::Change {
185190
old: old_record,
@@ -249,3 +254,42 @@ fn describe_same_content(from: &PackageRecord, to: &PackageRecord) -> bool {
249254
// Otherwise, just check that the name, version and build string match
250255
from.name == to.name && from.version == to.version && from.build == to.build
251256
}
257+
258+
#[cfg(test)]
259+
mod tests {
260+
use std::collections::HashSet;
261+
262+
use rattler_conda_types::Platform;
263+
264+
use crate::install::{
265+
test_utils::download_and_get_prefix_record, Transaction, TransactionOperation,
266+
};
267+
use assert_matches::assert_matches;
268+
269+
#[tokio::test]
270+
async fn test_reinstall_package() {
271+
let environment_dir = tempfile::TempDir::new().unwrap();
272+
let prefix_record = download_and_get_prefix_record(
273+
environment_dir.path(),
274+
"https://conda.anaconda.org/conda-forge/win-64/ruff-0.0.171-py310h298983d_0.conda"
275+
.parse()
276+
.unwrap(),
277+
"25c755b97189ee066576b4ae3999d5e7ff4406d236b984742194e63941838dcd",
278+
)
279+
.await;
280+
let name = prefix_record.repodata_record.package_record.name.clone();
281+
282+
let transaction = Transaction::from_current_and_desired(
283+
vec![prefix_record.clone()],
284+
vec![prefix_record.clone()],
285+
Some(HashSet::from_iter(vec![name])),
286+
Platform::current(),
287+
)
288+
.unwrap();
289+
290+
assert_matches!(
291+
transaction.operations[0],
292+
TransactionOperation::Change { .. }
293+
);
294+
}
295+
}

crates/rattler/src/install/unlink.rs

+11-47
Original file line numberDiff line numberDiff line change
@@ -227,59 +227,17 @@ mod tests {
227227
fs::{self, File},
228228
io::Write,
229229
path::Path,
230-
str::FromStr,
231230
};
232231

233-
use rattler_conda_types::{Platform, PrefixRecord, RepoDataRecord, Version};
234-
use url::Url;
232+
use rattler_conda_types::{Platform, RepoDataRecord};
235233

236-
use crate::{
237-
get_repodata_record,
238-
install::{
239-
empty_trash, link_package, unlink_package, InstallDriver, InstallOptions, PythonInfo,
240-
Transaction,
241-
},
242-
};
243-
244-
async fn link_ruff(target_prefix: &Path, package_url: Url, sha256_hash: &str) -> PrefixRecord {
245-
let package_path = tools::download_and_cache_file_async(package_url, sha256_hash)
246-
.await
247-
.unwrap();
248-
249-
let package_dir = tempfile::TempDir::new().unwrap();
250-
251-
// Create package cache
252-
rattler_package_streaming::fs::extract(&package_path, package_dir.path()).unwrap();
253-
254-
let py_info =
255-
PythonInfo::from_version(&Version::from_str("3.10").unwrap(), None, Platform::Linux64)
256-
.unwrap();
257-
let install_options = InstallOptions {
258-
python_info: Some(py_info),
259-
..InstallOptions::default()
260-
};
261-
262-
let install_driver = InstallDriver::default();
263-
// Link the package
264-
let paths = link_package(
265-
package_dir.path(),
266-
target_prefix,
267-
&install_driver,
268-
install_options,
269-
)
270-
.await
271-
.unwrap();
272-
273-
let repodata_record = get_repodata_record(&package_path);
274-
// Construct a PrefixRecord for the package
275-
276-
PrefixRecord::from_repodata_record(repodata_record, None, None, paths, None, None)
277-
}
234+
use crate::install::test_utils::download_and_get_prefix_record;
235+
use crate::install::{empty_trash, unlink_package, InstallDriver, Transaction};
278236

279237
#[tokio::test]
280238
async fn test_unlink_package() {
281239
let environment_dir = tempfile::TempDir::new().unwrap();
282-
let prefix_record = link_ruff(
240+
let prefix_record = download_and_get_prefix_record(
283241
environment_dir.path(),
284242
"https://conda.anaconda.org/conda-forge/win-64/ruff-0.0.171-py310h298983d_0.conda"
285243
.parse()
@@ -308,6 +266,7 @@ mod tests {
308266
let transaction = Transaction::from_current_and_desired(
309267
vec![prefix_record.clone()],
310268
Vec::<RepoDataRecord>::new().into_iter(),
269+
None,
311270
Platform::current(),
312271
)
313272
.unwrap();
@@ -328,7 +287,7 @@ mod tests {
328287
#[tokio::test]
329288
async fn test_unlink_package_python_noarch() {
330289
let target_prefix = tempfile::TempDir::new().unwrap();
331-
let prefix_record = link_ruff(
290+
let prefix_record = download_and_get_prefix_record(
332291
target_prefix.path(),
333292
"https://conda.anaconda.org/conda-forge/noarch/pytweening-1.0.4-pyhd8ed1ab_0.tar.bz2"
334293
.parse()
@@ -370,6 +329,7 @@ mod tests {
370329
let transaction = Transaction::from_current_and_desired(
371330
vec![prefix_record.clone()],
372331
Vec::<RepoDataRecord>::new().into_iter(),
332+
None,
373333
Platform::current(),
374334
)
375335
.unwrap();
@@ -404,6 +364,10 @@ mod tests {
404364
#[cfg(windows)]
405365
#[tokio::test]
406366
async fn test_unlink_package_in_use() {
367+
use crate::get_repodata_record;
368+
use crate::install::link_package;
369+
use crate::install::InstallOptions;
370+
use rattler_conda_types::PrefixRecord;
407371
use std::{
408372
env::{join_paths, split_paths, var_os},
409373
io::{BufRead, BufReader},

0 commit comments

Comments
 (0)