mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 10:04:52 +00:00
feat: support supplementary environment variables for hooks in App (#5916)
* feat: Implement launcher supplementary environmental variables, and env var substitution in hooks * lint fix, prettier reformat * descriptions added for each environment variable for hook settings * alphabetize language keys --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
use crate::state::{JavaVersion, MemorySettings};
|
||||
use regex::{Captures, Regex};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static ENV_VAR_PATTERN: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\$(\w+)").expect("valid env var regex"));
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HookVariables {
|
||||
pub instance_name: String,
|
||||
pub instance_id: String,
|
||||
pub instance_dir: String,
|
||||
pub java_path: String,
|
||||
pub java_args: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HookEnvironment {
|
||||
lookup_env: BTreeMap<String, String>,
|
||||
injected_env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl HookEnvironment {
|
||||
pub(crate) fn from_current_env(
|
||||
custom_env_vars: &[(String, String)],
|
||||
variables: HookVariables,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
std::env::vars_os().map(|(key, value)| {
|
||||
(
|
||||
key.to_string_lossy().into_owned(),
|
||||
value.to_string_lossy().into_owned(),
|
||||
)
|
||||
}),
|
||||
custom_env_vars,
|
||||
variables,
|
||||
)
|
||||
}
|
||||
|
||||
fn new(
|
||||
process_env: impl IntoIterator<Item = (String, String)>,
|
||||
custom_env_vars: &[(String, String)],
|
||||
variables: HookVariables,
|
||||
) -> Self {
|
||||
let mut lookup_env =
|
||||
process_env.into_iter().collect::<BTreeMap<_, _>>();
|
||||
let mut injected_env = BTreeMap::new();
|
||||
|
||||
for (key, value) in custom_env_vars {
|
||||
lookup_env.insert(key.clone(), value.clone());
|
||||
injected_env.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
let hook_vars = [
|
||||
("INST_NAME", variables.instance_name),
|
||||
("INST_ID", variables.instance_id),
|
||||
("INST_DIR", variables.instance_dir.clone()),
|
||||
("INST_MC_DIR", variables.instance_dir),
|
||||
("INST_JAVA", variables.java_path),
|
||||
("INST_JAVA_ARGS", variables.java_args),
|
||||
];
|
||||
|
||||
for (key, value) in hook_vars {
|
||||
let key = key.to_string();
|
||||
lookup_env.insert(key.clone(), value.clone());
|
||||
injected_env.insert(key, value);
|
||||
}
|
||||
|
||||
Self {
|
||||
lookup_env,
|
||||
injected_env,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand(&self, input: &str) -> String {
|
||||
ENV_VAR_PATTERN
|
||||
.replace_all(input, |captures: &Captures| {
|
||||
self.lookup_env
|
||||
.get(&captures[1])
|
||||
.cloned()
|
||||
.unwrap_or_else(|| captures[0].to_string())
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn injected_envs(&self) -> Vec<(String, String)> {
|
||||
self.injected_env
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_hook_java_args(
|
||||
java_args: &[String],
|
||||
memory: MemorySettings,
|
||||
java_version: &JavaVersion,
|
||||
) -> String {
|
||||
let mut args = vec![format!("-Xmx{}M", memory.maximum)];
|
||||
|
||||
args.extend(java_args.iter().filter(|arg| !arg.is_empty()).cloned());
|
||||
|
||||
if java_version.parsed_version >= 9 {
|
||||
args.push(
|
||||
"--add-opens=java.base/java.lang.reflect=ALL-UNNAMED".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if java_version.parsed_version >= 25 {
|
||||
args.push(
|
||||
"--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
args.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_variables() -> HookVariables {
|
||||
HookVariables {
|
||||
instance_name: "Test Instance".to_string(),
|
||||
instance_id: "test-instance".to_string(),
|
||||
instance_dir: "/profiles/test-instance".to_string(),
|
||||
java_path: "/java/bin/java".to_string(),
|
||||
java_args: "-Xmx4096M".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expands_builtin_and_custom_variables() {
|
||||
let env = HookEnvironment::new(
|
||||
[("HOME".to_string(), "/home/alex".to_string())],
|
||||
&[("CUSTOM_VAR".to_string(), "custom".to_string())],
|
||||
sample_variables(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
env.expand("$HOME/$INST_ID/$CUSTOM_VAR"),
|
||||
"/home/alex/test-instance/custom"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_unknown_variables_untouched() {
|
||||
let env = HookEnvironment::new([], &[], sample_variables());
|
||||
|
||||
assert_eq!(env.expand("$UNKNOWN/$INST_NAME"), "$UNKNOWN/Test Instance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expands_empty_variables_to_empty_strings() {
|
||||
let env = HookEnvironment::new(
|
||||
[("EMPTY_VAR".to_string(), String::new())],
|
||||
&[],
|
||||
sample_variables(),
|
||||
);
|
||||
|
||||
assert_eq!(env.expand("prefix$EMPTY_VAR-suffix"), "prefix-suffix");
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
|
||||
mod args;
|
||||
pub(crate) mod hooks;
|
||||
|
||||
pub mod download;
|
||||
pub mod quick_play_version;
|
||||
@@ -214,6 +215,62 @@ fn loader_versions_for_game_version<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_java_for_launch(
|
||||
context: &InstanceLaunchContext,
|
||||
) -> crate::Result<JavaVersion> {
|
||||
let state = State::get().await?;
|
||||
let content_set = &context.applied_content_set;
|
||||
let (minecraft, version_index) =
|
||||
resolve_minecraft_manifest(&content_set.game_version, &state).await?;
|
||||
let version = &minecraft.versions[version_index];
|
||||
|
||||
let mut loader_version = get_loader_version_from_profile(
|
||||
&content_set.game_version,
|
||||
content_set.loader,
|
||||
content_set.loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if content_set.loader != ModLoader::Vanilla && loader_version.is_none() {
|
||||
loader_version = get_loader_version_from_profile(
|
||||
&content_set.game_version,
|
||||
content_set.loader,
|
||||
Some("stable"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let version_info = download::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
loader_version.as_ref(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let key = version_info
|
||||
.java_version
|
||||
.as_ref()
|
||||
.map_or(8, |it| it.major_version);
|
||||
let (java_path, set_java) = if let Some(java_version) =
|
||||
get_java_version_from_launch_context(context, &version_info).await?
|
||||
{
|
||||
(PathBuf::from(java_version.path), false)
|
||||
} else {
|
||||
(crate::api::jre::auto_install_java(key).await?, true)
|
||||
};
|
||||
|
||||
let java_version = crate::api::jre::check_jre(java_path).await?;
|
||||
|
||||
if set_java {
|
||||
java_version.upsert(&state.pool).await?;
|
||||
}
|
||||
|
||||
Ok(java_version)
|
||||
}
|
||||
|
||||
/// Resolves the Minecraft version manifest and finds the index for the given
|
||||
/// game version. If the version isn't found in the cache, forces a manifest
|
||||
/// refresh to pick up newly-released versions.
|
||||
@@ -998,7 +1055,7 @@ pub async fn launch_minecraft(
|
||||
// Java options should be set in instance options (the existence of _JAVA_OPTIONS overwrites them)
|
||||
command.env_remove("_JAVA_OPTIONS");
|
||||
|
||||
command.envs(env_args);
|
||||
command.envs(env_args.iter().cloned());
|
||||
|
||||
// Overwrites the minecraft options.txt file with the settings from the profile
|
||||
// Uses 'a:b' syntax which is not quite yaml
|
||||
@@ -1084,6 +1141,7 @@ pub async fn launch_minecraft(
|
||||
&instance.name,
|
||||
command,
|
||||
post_exit_hook,
|
||||
env_args,
|
||||
state.directories.instance_logs_dir(&instance.path),
|
||||
version_info.logging.is_some(),
|
||||
main_class_keep_alive,
|
||||
|
||||
Reference in New Issue
Block a user