Skip to content
Merged
Show file tree
Hide file tree
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: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: hyperion
Title: Pharmaceutical Model Development and Workflow Tools
Version: 0.5.0
Version: 0.4.2.9000
Authors@R: c(
person("Matt", "Smith", , "matthews@a2-ai.com", role = c("aut", "cre")),
person("Vincent", "Prouillet", ,"vincent@a2-ai.com", role = c("aut")),
Expand All @@ -18,6 +18,7 @@ License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
Config/rextendr/version: 0.5.0
Config/PharosVersion: 0.5.0
Config/build/never-clean: true
Config/build/extra-sources: src/rust/Cargo.lock
SystemRequirements: Cargo (Rust's package manager), rustc >= 1.65.0, xz
Expand Down
106 changes: 97 additions & 9 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,38 @@ print_data_table_knit <- function(formatted_data, title) {
return(output)
}

#' Detect the installed pharos CLI and its version
#'
#' Locates the `pharos` executable on the user's PATH via [Sys.which()] and
#' parses the version reported by `pharos --version`.
#'
#' @return A list with elements:
#' \itemize{
#' \item `path` — absolute path to the pharos binary, or `NA_character_` if not found
#' \item `version` — installed version string (e.g. `"0.4.2"`), or `NA_character_` if not found or unparseable
#' }
#' @keywords internal
#' @noRd
detect_pharos <- function() {
path <- unname(Sys.which("pharos"))
if (!nzchar(path)) {
return(list(path = NA_character_, version = NA_character_))
}

out <- tryCatch(
suppressWarnings(system2(path, "--version", stdout = TRUE, stderr = TRUE)),
error = function(e) NULL
)
if (is.null(out) || !length(out)) {
return(list(path = path, version = NA_character_))
}

match <- regmatches(out[1], regexpr("[0-9]+\\.[0-9]+\\.[0-9]+", out[1]))
version <- if (length(match)) match else NA_character_

list(path = path, version = version)
}

#' Generates a tidyverse-esque onAttach message for hyperion options
#'
#' @return a message to display on attach
Expand Down Expand Up @@ -456,6 +488,13 @@ hyperion_options_message <- function() {
# Check pharos config file status
pharos_config_status <- find_pharos_config_file()

# Detect installed pharos CLI and compare against expected version
pharos_cli <- detect_pharos()
expected_pharos <- utils::packageDescription(
"hyperion",
fields = "Config/PharosVersion"
)

# Format .onAttach message
msg <- "\n\n"

Expand All @@ -468,24 +507,55 @@ hyperion_options_message <- function() {
"\n"
)

# Show the hyperion.config_dir override status first since it controls
# where pharos.toml gets resolved from.
config_dir_value <- getOption("hyperion.config_dir")
if (!is.null(config_dir_value) && nzchar(config_dir_value)) {
# Pharos CLI: absent, mismatched, or matched
if (is.na(pharos_cli$path)) {
msg <- paste0(
msg,
cli::col_green(cli::symbol$tick),
cli::col_red(cli::symbol$cross),
" ",
"hyperion.config_dir : ",
config_dir_value,
cli::col_red("pharos CLI not found on PATH"),
"\n"
)
} else if (is.na(pharos_cli$version)) {
msg <- paste0(
msg,
cli::col_red(cli::symbol$cross),
" ",
cli::col_red(paste0(
"pharos CLI found at ",
pharos_cli$path,
" but version could not be determined"
)),
"\n"
)
} else if (
!is.na(expected_pharos) && !identical(pharos_cli$version, expected_pharos)
) {
msg <- paste0(
msg,
cli::col_red(cli::symbol$cross),
" ",
cli::col_red(paste0(
"pharos CLI version mismatch: installed ",
pharos_cli$version,
", expected ",
expected_pharos,
" (",
pharos_cli$path,
")"
)),
"\n"
)
} else {
msg <- paste0(
msg,
cli::symbol$info,
cli::col_green(cli::symbol$tick),
" ",
cli::style_dim("hyperion.config_dir : (unset)"),
"pharos CLI: ",
pharos_cli$version,
" (",
pharos_cli$path,
")",
"\n"
)
}
Expand All @@ -509,6 +579,24 @@ hyperion_options_message <- function() {
)
}

# hyperion.config_dir is shown as a sub-detail of pharos.toml resolution
# since it controls where pharos.toml is looked up from.
config_dir_value <- getOption("hyperion.config_dir")
if (!is.null(config_dir_value) && nzchar(config_dir_value)) {
msg <- paste0(
msg,
" \u2514 hyperion.config_dir : ",
config_dir_value,
"\n"
)
} else {
msg <- paste0(
msg,
cli::style_dim(" \u2514 hyperion.config_dir : (unset)"),
"\n"
)
}

# Add general options section
if (length(set_general_options)) {
msg <- paste0(
Expand Down
20 changes: 10 additions & 10 deletions src/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ serde = { workspace = true }
extendr-api = { version = "0.9.0", features = ["serde"] }

# Pharos components
nonmem = { git = "https://github.com/a2-ai/pharos", package = "nonmem", tag = "v0.5.0" }
nmparser = { git = "https://github.com/a2-ai/pharos", package = "nonmem-parser", tag = "v0.5.0" }
config = { git = "https://github.com/a2-ai/pharos", package = "config", tag = "v0.5.0" }
scheduler = { git = "https://github.com/a2-ai/pharos", package = "scheduler", tag = "v0.5.0" }
nonmem = { git = "https://github.com/a2-ai/pharos", package = "nonmem", tag = "v0.5.1" }
nmparser = { git = "https://github.com/a2-ai/pharos", package = "nonmem-parser", tag = "v0.5.1" }
config = { git = "https://github.com/a2-ai/pharos", package = "config", tag = "v0.5.1" }
scheduler = { git = "https://github.com/a2-ai/pharos", package = "scheduler", tag = "v0.5.1" }

# Core utilities
anyhow = "1.0.100"
Expand Down
2 changes: 1 addition & 1 deletion src/rust/nonmem/src/model/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ pub fn copy_model_wrap(
.ok_or_extendr_err("Could not determine parent directory")?
.join(&model_stem);
let content = fs::read_to_string(&from_path).map_to_extendr_err("")?;
let model = Model::parse(&content)
let model = Model::parse(&from_path, &content)
.map_err(|_| extendr_err!("Failed to parse model: {}", from_path.display()))?;
resolve_ext_path(&model, &run_dir, &model_stem)
}
Expand Down
12 changes: 3 additions & 9 deletions src/rust/nonmem/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,11 @@ pub fn read_model(path: &str) -> Result<Robj> {
let mod_path = validate_model_path(path)?;
let content = fs::read_to_string(&mod_path).map_to_extendr_err("")?;

let mut model = match Model::parse(&content) {
let mut model = match Model::parse(path, &content) {
Ok(model) => model,
Err(diags) => {
let msg = diags
.iter()
.map(|d| d.render(mod_path.as_path(), &content))
.collect::<Vec<_>>()
.join("\n");
return Err(extendr_err!("Failed to read model file:\n{msg}"));
}
Err(e) => return Err(extendr_err!("Failed to read model file:\n{e}")),
};

let mut robj_model = model_to_robj(&mut model, &mod_path)?;
add_run_status_attr(&mut robj_model, &mod_path)?;

Expand Down
7 changes: 4 additions & 3 deletions src/rust/nonmem/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ fn assemble_top_level(model: Model, source: PathBuf) -> Result<ModelLocation> {

fn parse_model_from_disk(path: &Path) -> Result<Model> {
let content = fs::read_to_string(path).map_to_extendr_err("Failed to read model file")?;
Model::parse(&content).map_err(|_| extendr_err!("Failed to parse model: {}", path.display()))
Model::parse(path, &content)
.map_err(|_| extendr_err!("Failed to parse model: {}", path.display()))
}

/// Resolve the final `.ext` file path for a model, honoring `$EST FILE=`.
Expand Down Expand Up @@ -348,8 +349,8 @@ pub fn try_parse_model(path: &str) -> Option<Model> {
let model_path = find_output_file(search_path, "mod")
.or_else(|_| find_output_file(path, "ctl"))
.ok()?;
let content = fs::read_to_string(model_path).ok()?;
Model::parse(&content).ok()
let content = fs::read_to_string(&model_path).ok()?;
Model::parse(model_path, &content).ok()
}

/// Gets the comment type from pharos.toml configuration
Expand Down
Loading