Skip to content

Rv crate #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/activate.rs
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ use crate::consts::{ACTIVATE_FILE_TEMPLATE, RVR_FILE_CONTENT};
const ACTIVATE_FILE_NAME: &str = "rv/scripts/activate.R";
const RVR_FILE_NAME: &str = "rv/scripts/rvr.R";

/// activate a directory for use with rv by sourcing scripts in `rv/scripts` within the .Rprofile
/// This will set the repositories, libPaths, and create a .rv R environment (if no_r_environment is not false)
pub fn activate(dir: impl AsRef<Path>, no_r_environment: bool) -> Result<(), ActivateError> {
let dir = dir.as_ref();

@@ -53,6 +55,7 @@ fn add_rprofile_source_call(
Ok(())
}

/// Deactivate a directory for use with rv by removing the scripts in `rv/scripts` sourced within the .Rprofile
pub fn deactivate(dir: impl AsRef<Path>) -> Result<(), ActivateError> {
let dir = dir.as_ref();
let rprofile_path = dir.join(".Rprofile");
34 changes: 23 additions & 11 deletions src/add.rs
Original file line number Diff line number Diff line change
@@ -5,18 +5,28 @@ use toml_edit::{Array, DocumentMut, Formatted, Value};

use crate::{config::ConfigLoadError, Config};

pub fn read_and_verify_config(config_file: impl AsRef<Path>) -> Result<DocumentMut, AddError> {
let config_file = config_file.as_ref();
let _ = Config::from_file(config_file).map_err(|e| AddError {
path: config_file.into(),

/// Add packages to the config file at path as Simple ConfigDependencies
pub fn add_packages(path: impl AsRef<Path>, packages: Vec<String>) -> Result<String, AddError> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.map_err(|e| AddError {
path: path.into(),
source: Box::new(AddErrorKind::Io(e)),
})?;
// Verify the config file is valid before parsing as DocumentMut
content.parse::<Config>().map_err(|e| AddError {
path: path.into(),
source: Box::new(AddErrorKind::ConfigLoad(e)),
})?;
let config_content = fs::read_to_string(config_file).unwrap(); // Verified config could be loaded above

Ok(config_content.parse::<DocumentMut>().unwrap()) // Verify config was valid toml above
let mut config_doc = content.parse::<DocumentMut>().unwrap();
add_pkgs_to_config(&mut config_doc, packages)?;

Ok(config_doc.to_string())
}

pub fn add_packages(config_doc: &mut DocumentMut, packages: Vec<String>) -> Result<(), AddError> {
fn add_pkgs_to_config(config_doc: &mut DocumentMut, packages: Vec<String>) -> Result<(), AddError> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the read_and_verify_config was implemented from this convo #107 (comment).

I abstracted one level so that it keeps a fxn taking it a DocumentMut, but can change it to whatever you feel is best

#107 (comment)

// get the dependencies array
let config_deps = get_mut_array(config_doc);

@@ -85,13 +95,15 @@ pub enum AddErrorKind {

#[cfg(test)]
mod tests {
use crate::{add_packages, read_and_verify_config};
use std::fs;

use super::add_pkgs_to_config;

#[test]
fn add_remove() {
let config_file = "src/tests/valid_config/all_fields.toml";
let mut doc = read_and_verify_config(&config_file).unwrap();
add_packages(&mut doc, vec!["pkg1".to_string(), "pkg2".to_string()]).unwrap();
let config_file = fs::read_to_string("src/tests/valid_config/all_fields.toml").unwrap();
let mut doc = config_file.parse::<toml_edit::DocumentMut>().unwrap();
add_pkgs_to_config(&mut doc, vec!["pkg1".to_string(), "pkg2".to_string()]).unwrap();
insta::assert_snapshot!("add_remove", doc.to_string());
}
}
5 changes: 4 additions & 1 deletion src/cache/disk.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(missing_docs)]

use std::error::Error;
use std::fmt;
use std::fmt::Formatter;
@@ -13,6 +15,7 @@ use crate::lockfile::Source;
use crate::{SystemInfo, Version};

#[derive(Debug, Clone)]
/// Expected paths to the package in the cache for both source and binary. Does not guarantee a package will exist at the location
pub struct PackagePaths {
pub binary: PathBuf,
pub source: PathBuf,
@@ -72,7 +75,6 @@ pub struct DiskCache {
}

impl DiskCache {
/// Instantiate our cache abstraction.
pub fn new(
r_version: &Version,
system_info: SystemInfo,
@@ -139,6 +141,7 @@ impl DiskCache {
self.root.join("urls").join(encoded)
}

/// Get the path to the cloned git repo
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the comment is redundant with the function name, you can #![allow(missing_docs)] rather than repetitive comments

pub fn get_git_clone_path(&self, repo_url: &str) -> PathBuf {
let encoded = hash_string(&repo_url.trim_end_matches("/").to_ascii_lowercase());
self.root.join("git").join(encoded)
2 changes: 2 additions & 0 deletions src/cache/info.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(missing_docs)]

use std::fmt;
use std::path::PathBuf;

2 changes: 2 additions & 0 deletions src/cli/commands/init.rs
Original file line number Diff line number Diff line change
@@ -96,6 +96,7 @@ fn render_config(
.replace("%dependencies%", &deps)
}

/// Find the repositories set in `options("repos")` in R
pub fn find_r_repositories() -> Result<Vec<Repository>, InitError> {
let r_code = r#"
repos <- getOption("repos")
@@ -160,6 +161,7 @@ fn strip_linux_url(url: &str) -> String {
new_url.join("/")
}

/// initialize the rv directory, including the library directory and the .gitignore file
pub fn init_structure(project_directory: impl AsRef<Path>) -> Result<(), InitError> {
let project_directory = project_directory.as_ref();
create_library_structure(project_directory)?;
3 changes: 3 additions & 0 deletions src/cli/commands/migrate.rs
Original file line number Diff line number Diff line change
@@ -22,6 +22,9 @@ dependencies = [
]
"#;

/// Migrate a renv project to rv
/// This will create a config file in the specified location, and return the unresolved dependencies.
/// Having unresolved dependencies will not cause an error since the migration is a best effort starter and manual massaging is expected
pub fn migrate_renv(
renv_file: impl AsRef<Path>,
config_file: impl AsRef<Path>,
2 changes: 2 additions & 0 deletions src/cli/context.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(missing_docs)]
//! CLI context that gets instantiated for a few commands and passed around
use std::path::{Path, PathBuf};

@@ -73,6 +74,7 @@ impl CliContext {
Ok(())
}

/// Only load the databases if the lockfile does not fully resolve the dependencies
pub fn load_databases_if_needed(&mut self) -> Result<()> {
let can_resolve = self
.lockfile
1 change: 1 addition & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod commands;
mod context;
/// Utilities used for the cli
pub mod utils;

pub use commands::{find_r_repositories, init, init_structure, migrate_renv};
2 changes: 2 additions & 0 deletions src/cli/utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(missing_docs)]
pub fn write_err(err: &(dyn std::error::Error + 'static)) -> String {
let mut out = format!("{err}");

@@ -11,6 +12,7 @@ pub fn write_err(err: &(dyn std::error::Error + 'static)) -> String {
}

#[macro_export]
/// execute a block of code, and log the time the command took in milliseconds
macro_rules! timeit {
($msg:expr, $x:expr) => {{
let start = std::time::Instant::now();
22 changes: 20 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(missing_docs)]

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -43,34 +45,42 @@ impl Repository {
#[serde(untagged)]
#[serde(deny_unknown_fields)]
pub enum ConfigDependency {
/// A simple dependency, no additional configuration needed. To be resolved as a repository package
Simple(String),
/// One of commit, tag, branch must be specified
Git {
git: String,
// TODO: validate that either commit, branch or tag is set
commit: Option<String>,
tag: Option<String>,
branch: Option<String>,
/// A path to a subdirectory containing the R package
directory: Option<String>,
/// Must match the package found in the repo
name: String,
#[serde(default)]
install_suggestions: bool,
},
/// Can be a directory or tarball, source or binary
Local {
path: PathBuf,
/// Must match the package found at the path
name: String,
#[serde(default)]
install_suggestions: bool,
},
/// Can be a source or binary tarball
Url {
url: String,
/// Must match the package found at the url
name: String,
#[serde(default)]
install_suggestions: bool,
#[serde(default)]
force_source: Option<bool>,
},
/// Additional configuration for a dependency being sourced from a repository
Detailed {
name: String,
/// repository alias in the config gets replaced by the URL as part of
repository: Option<String>,
#[serde(default)]
install_suggestions: bool,
@@ -80,6 +90,7 @@ pub enum ConfigDependency {
}

impl ConfigDependency {
/// Get the name of the dependency
pub fn name(&self) -> &str {
match self {
ConfigDependency::Simple(s) => s,
@@ -90,27 +101,31 @@ impl ConfigDependency {
}
}

/// Get whether the package has force_source set if available
pub fn force_source(&self) -> Option<bool> {
match self {
ConfigDependency::Detailed { force_source, .. } => *force_source,
_ => None,
}
}

/// Get the repository alias if available
pub fn r_repository(&self) -> Option<&str> {
match self {
ConfigDependency::Detailed { repository, .. } => repository.as_deref(),
_ => None,
}
}

/// Get the local path if available
pub fn local_path(&self) -> Option<PathBuf> {
match self {
ConfigDependency::Local { path, .. } => Some(path.clone()),
_ => None,
}
}

/// Convert a Git dependency to a Source specified by the sha
pub(crate) fn as_git_source_with_sha(&self, sha: String) -> Source {
// git: String,
// // TODO: validate that either commit, branch or tag is set
@@ -136,6 +151,7 @@ impl ConfigDependency {
}
}

/// Get whether the package has install_suggestions set
pub fn install_suggestions(&self) -> bool {
match self {
ConfigDependency::Simple(_) => false,
@@ -193,13 +209,15 @@ pub(crate) struct Project {
prefer_repositories_for: Vec<String>,
}

/// The config file is a TOML table, project
#[derive(Debug, PartialEq, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub(crate) project: Project,
}

impl Config {
/// Create a new config from a file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigLoadError> {
let content = match std::fs::read_to_string(path.as_ref()) {
Ok(c) => c,
3 changes: 2 additions & 1 deletion src/consts.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#![allow(missing_docs)]

pub const PACKAGE_FILENAME: &str = "PACKAGES";
pub const DESCRIPTION_FILENAME: &str = "DESCRIPTION";
pub const SOURCE_PACKAGES_PATH: &str = "/src/contrib/PACKAGES";
pub const LOCKFILE_NAME: &str = "rv.lock";

pub const RV_DIR_NAME: &str = "rv";
pub const LIBRARY_ROOT_DIR_NAME: &str = "library";
pub const STAGING_DIR_NAME: &str = "staging";
4 changes: 2 additions & 2 deletions src/git/local.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use std::path::{Path, PathBuf};
use std::process::Command;

@@ -214,7 +215,6 @@ impl GitRepository {
self.checkout_branch(&branch_name)
}

/// Checks if we have that reference in the local repo.
pub fn get_description_file_content(
&self,
url: &str,
@@ -244,7 +244,7 @@ impl GitRepository {
))
}

/// Does a sparse checkout with just DESCRIPTION file checkout.
/// Does a sparse checkout of just DESCRIPTION file checkout to speed up resolution
pub fn sparse_checkout(
&self,
url: &str,
1 change: 1 addition & 0 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use std::process::Command;

mod local;
4 changes: 1 addition & 3 deletions src/git/reference.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
#![allow(missing_docs)]
use std::fmt;

/// What a git URL can point to
/// If it's coming from a lockfile, it will always be a commit
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GitReference<'g> {
/// A specific branch
Branch(&'g str),
/// A specific tag.
Tag(&'g str),
/// The commit hash
Commit(&'g str),
/// We don't know what it is.
/// Used for Remotes
3 changes: 3 additions & 0 deletions src/http.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(missing_docs)]

use std::io::{BufReader, Cursor};
use std::path::{Path, PathBuf};
use std::time::Instant;
@@ -97,6 +99,7 @@ pub enum HttpErrorKind {
Http(u16),
}

/// Trait for downloading files over HTTP
pub trait HttpDownload {
/// Downloads a file to the given writer and returns how many bytes were read
fn download<W: Write>(
Loading