mirror of
https://github.com/modrinth/code.git
synced 2026-08-24 16:44:51 +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:
@@ -116,6 +116,35 @@ const messages = defineMessages({
|
|||||||
id: 'app.settings.default-instance-options.post-exit-hook.description',
|
id: 'app.settings.default-instance-options.post-exit-hook.description',
|
||||||
defaultMessage: 'Runs after the game closes.',
|
defaultMessage: 'Runs after the game closes.',
|
||||||
},
|
},
|
||||||
|
hookVariablesDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Hooks run in the working directory of the instance, with the following variables:',
|
||||||
|
},
|
||||||
|
instanceNameDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-name.description',
|
||||||
|
defaultMessage: '$INST_NAME: The name of the instance',
|
||||||
|
},
|
||||||
|
instanceIdDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-id.description',
|
||||||
|
defaultMessage: "$INST_ID: The name of the instance's folder",
|
||||||
|
},
|
||||||
|
instanceDirDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-dir.description',
|
||||||
|
defaultMessage: "$INST_DIR: The absolute path to the instance's folder",
|
||||||
|
},
|
||||||
|
instanceMcDirDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-mc-dir.description',
|
||||||
|
defaultMessage: '$INST_MC_DIR: An alias for $INST_DIR',
|
||||||
|
},
|
||||||
|
instanceJavaDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-java.description',
|
||||||
|
defaultMessage: '$INST_JAVA: The absolute path to the java binary',
|
||||||
|
},
|
||||||
|
instanceJavaArgsDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-java-args.description',
|
||||||
|
defaultMessage: '$INST_JAVA_ARGS: The JVM Arguments provided to the game',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchSettings = await get()
|
const fetchSettings = await get()
|
||||||
@@ -328,6 +357,18 @@ watch(
|
|||||||
{{ formatMessage(messages.postExitHookDescription) }}
|
{{ formatMessage(messages.postExitHookDescription) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="m-0 leading-tight">
|
||||||
|
{{ formatMessage(messages.hookVariablesDescription) }}
|
||||||
|
<ul>
|
||||||
|
<li>{{ formatMessage(messages.instanceNameDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceIdDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceDirDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceMcDirDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceJavaDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceJavaArgsDescription) }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1625,6 +1625,27 @@
|
|||||||
"instance.settings.tabs.hooks.title": {
|
"instance.settings.tabs.hooks.title": {
|
||||||
"message": "Game launch hooks"
|
"message": "Game launch hooks"
|
||||||
},
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.description": {
|
||||||
|
"message": "Hooks run in the working directory of the instance, with the following variables:"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.inst-dir.description": {
|
||||||
|
"message": "$INST_DIR: The absolute path to the instance's folder"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.inst-id.description": {
|
||||||
|
"message": "$INST_ID: The name of the instance's folder"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.inst-java-args.description": {
|
||||||
|
"message": "$INST_JAVA_ARGS: The JVM Arguments provided to the game"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.inst-java.description": {
|
||||||
|
"message": "$INST_JAVA: The absolute path to the java binary"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.inst-mc-dir.description": {
|
||||||
|
"message": "$INST_MC_DIR: An alias for $INST_DIR"
|
||||||
|
},
|
||||||
|
"instance.settings.tabs.hooks.variables.inst-name.description": {
|
||||||
|
"message": "$INST_NAME: The name of the instance"
|
||||||
|
},
|
||||||
"instance.settings.tabs.hooks.wrapper": {
|
"instance.settings.tabs.hooks.wrapper": {
|
||||||
"message": "Wrapper"
|
"message": "Wrapper"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,6 +56,35 @@ const messages = defineMessages({
|
|||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
'Hooks allow advanced users to run certain system commands before and after launching the game.',
|
||||||
},
|
},
|
||||||
|
hookVariablesDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.description',
|
||||||
|
defaultMessage:
|
||||||
|
'Hooks run in the working directory of the instance, with the following variables:',
|
||||||
|
},
|
||||||
|
instanceNameDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-name.description',
|
||||||
|
defaultMessage: '$INST_NAME: The name of the instance',
|
||||||
|
},
|
||||||
|
instanceIdDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-id.description',
|
||||||
|
defaultMessage: "$INST_ID: The name of the instance's folder",
|
||||||
|
},
|
||||||
|
instanceDirDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-dir.description',
|
||||||
|
defaultMessage: "$INST_DIR: The absolute path to the instance's folder",
|
||||||
|
},
|
||||||
|
instanceMcDirDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-mc-dir.description',
|
||||||
|
defaultMessage: '$INST_MC_DIR: An alias for $INST_DIR',
|
||||||
|
},
|
||||||
|
instanceJavaDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-java.description',
|
||||||
|
defaultMessage: '$INST_JAVA: The absolute path to the java binary',
|
||||||
|
},
|
||||||
|
instanceJavaArgsDescription: {
|
||||||
|
id: 'instance.settings.tabs.hooks.variables.inst-java-args.description',
|
||||||
|
defaultMessage: '$INST_JAVA_ARGS: The JVM Arguments provided to the game',
|
||||||
|
},
|
||||||
customHooks: {
|
customHooks: {
|
||||||
id: 'instance.settings.tabs.hooks.custom-hooks',
|
id: 'instance.settings.tabs.hooks.custom-hooks',
|
||||||
defaultMessage: 'Custom launch hooks',
|
defaultMessage: 'Custom launch hooks',
|
||||||
@@ -153,5 +182,17 @@ const messages = defineMessages({
|
|||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
{{ formatMessage(messages.postExitDescription) }}
|
{{ formatMessage(messages.postExitDescription) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="m-0 mt-6">
|
||||||
|
{{ formatMessage(messages.hookVariablesDescription) }}
|
||||||
|
</div>
|
||||||
|
<ul class="m-0 mt-2">
|
||||||
|
<li>{{ formatMessage(messages.instanceNameDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceIdDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceDirDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceMcDirDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceJavaDescription) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceJavaArgsDescription) }}</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -79,15 +79,95 @@ async fn run_credentials(
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let pre_launch_hooks = context
|
let pre_launch_hook = context
|
||||||
.launch_overrides
|
.launch_overrides
|
||||||
.hooks
|
.hooks
|
||||||
.pre_launch
|
.pre_launch
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.or(settings.hooks.pre_launch.as_ref())
|
.or(settings.hooks.pre_launch.as_ref())
|
||||||
.filter(|hook_command| !hook_command.is_empty());
|
.filter(|hook_command| !hook_command.is_empty());
|
||||||
if let Some(hook) = pre_launch_hooks {
|
|
||||||
let mut cmd = shlex::split(hook)
|
let java_args = context
|
||||||
|
.launch_overrides
|
||||||
|
.extra_launch_args
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(settings.extra_launch_args);
|
||||||
|
|
||||||
|
let wrapper = context
|
||||||
|
.launch_overrides
|
||||||
|
.hooks
|
||||||
|
.wrapper
|
||||||
|
.clone()
|
||||||
|
.or(settings.hooks.wrapper)
|
||||||
|
.filter(|hook_command| !hook_command.is_empty());
|
||||||
|
|
||||||
|
let env_args = context
|
||||||
|
.launch_overrides
|
||||||
|
.custom_env_vars
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(settings.custom_env_vars);
|
||||||
|
|
||||||
|
let post_exit_hook = context
|
||||||
|
.launch_overrides
|
||||||
|
.hooks
|
||||||
|
.post_exit
|
||||||
|
.clone()
|
||||||
|
.or(settings.hooks.post_exit)
|
||||||
|
.filter(|hook_command| !hook_command.is_empty());
|
||||||
|
|
||||||
|
let memory = context.launch_overrides.memory.unwrap_or(settings.memory);
|
||||||
|
let resolution = context
|
||||||
|
.launch_overrides
|
||||||
|
.game_resolution
|
||||||
|
.unwrap_or(settings.game_resolution);
|
||||||
|
let has_hook_commands = pre_launch_hook.is_some()
|
||||||
|
|| wrapper.is_some()
|
||||||
|
|| post_exit_hook.is_some();
|
||||||
|
let full_path = if has_hook_commands {
|
||||||
|
Some(crate::util::io::canonicalize(
|
||||||
|
state
|
||||||
|
.directories
|
||||||
|
.instances_dir()
|
||||||
|
.join(&context.instance.path),
|
||||||
|
)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let hook_environment = if has_hook_commands {
|
||||||
|
let full_path = full_path
|
||||||
|
.as_ref()
|
||||||
|
.expect("hooked launches always resolve their instance path");
|
||||||
|
let java_version =
|
||||||
|
crate::launcher::resolve_java_for_launch(&context).await?;
|
||||||
|
|
||||||
|
Some(crate::launcher::hooks::HookEnvironment::from_current_env(
|
||||||
|
&env_args,
|
||||||
|
crate::launcher::hooks::HookVariables {
|
||||||
|
instance_name: context.instance.name.clone(),
|
||||||
|
instance_id: context.instance.path.clone(),
|
||||||
|
instance_dir: full_path.to_string_lossy().to_string(),
|
||||||
|
java_path: java_version.path.clone(),
|
||||||
|
java_args: crate::launcher::hooks::build_hook_java_args(
|
||||||
|
&java_args,
|
||||||
|
memory,
|
||||||
|
&java_version,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let launch_env_args = hook_environment
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| env_args.clone(), |env| env.injected_envs());
|
||||||
|
|
||||||
|
if let (Some(hook), Some(hook_environment), Some(full_path)) = (
|
||||||
|
pre_launch_hook,
|
||||||
|
hook_environment.as_ref(),
|
||||||
|
full_path.as_ref(),
|
||||||
|
) {
|
||||||
|
let expanded_hook = hook_environment.expand(hook);
|
||||||
|
let mut cmd = shlex::split(&expanded_hook)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Invalid pre-launch command: {hook}",
|
"Invalid pre-launch command: {hook}",
|
||||||
@@ -96,17 +176,12 @@ async fn run_credentials(
|
|||||||
.into_iter();
|
.into_iter();
|
||||||
|
|
||||||
if let Some(command) = cmd.next() {
|
if let Some(command) = cmd.next() {
|
||||||
let full_path = crate::util::io::canonicalize(
|
|
||||||
state
|
|
||||||
.directories
|
|
||||||
.instances_dir()
|
|
||||||
.join(&context.instance.path),
|
|
||||||
)?;
|
|
||||||
let result = Command::new(command)
|
let result = Command::new(command)
|
||||||
.args(cmd)
|
.args(cmd)
|
||||||
.current_dir(&full_path)
|
.envs(launch_env_args.iter().cloned())
|
||||||
|
.current_dir(full_path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| IOError::with_path(e, &full_path))?
|
.map_err(|e| IOError::with_path(e, full_path))?
|
||||||
.wait()
|
.wait()
|
||||||
.await
|
.await
|
||||||
.map_err(IOError::from)?;
|
.map_err(IOError::from)?;
|
||||||
@@ -121,34 +196,19 @@ async fn run_credentials(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let java_args = context
|
let wrapper = wrapper
|
||||||
.launch_overrides
|
.map(|hook| {
|
||||||
.extra_launch_args
|
hook_environment
|
||||||
.clone()
|
.as_ref()
|
||||||
.unwrap_or(settings.extra_launch_args);
|
.map_or(hook.clone(), |env| env.expand(&hook))
|
||||||
let wrapper = context
|
})
|
||||||
.launch_overrides
|
|
||||||
.hooks
|
|
||||||
.wrapper
|
|
||||||
.clone()
|
|
||||||
.or(settings.hooks.wrapper)
|
|
||||||
.filter(|hook_command| !hook_command.is_empty());
|
.filter(|hook_command| !hook_command.is_empty());
|
||||||
let memory = context.launch_overrides.memory.unwrap_or(settings.memory);
|
let post_exit_hook = post_exit_hook
|
||||||
let resolution = context
|
.map(|hook| {
|
||||||
.launch_overrides
|
hook_environment
|
||||||
.game_resolution
|
.as_ref()
|
||||||
.unwrap_or(settings.game_resolution);
|
.map_or(hook.clone(), |env| env.expand(&hook))
|
||||||
let env_args = context
|
})
|
||||||
.launch_overrides
|
|
||||||
.custom_env_vars
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(settings.custom_env_vars);
|
|
||||||
let post_exit_hook = context
|
|
||||||
.launch_overrides
|
|
||||||
.hooks
|
|
||||||
.post_exit
|
|
||||||
.clone()
|
|
||||||
.or(settings.hooks.post_exit)
|
|
||||||
.filter(|hook_command| !hook_command.is_empty());
|
.filter(|hook_command| !hook_command.is_empty());
|
||||||
|
|
||||||
let mut mc_set_options: Vec<(String, String)> = vec![];
|
let mut mc_set_options: Vec<(String, String)> = vec![];
|
||||||
@@ -210,7 +270,7 @@ async fn run_credentials(
|
|||||||
crate::minecraft_skins::flush_pending_skin_change().await?;
|
crate::minecraft_skins::flush_pending_skin_change().await?;
|
||||||
crate::launcher::launch_minecraft(
|
crate::launcher::launch_minecraft(
|
||||||
&java_args,
|
&java_args,
|
||||||
&env_args,
|
&launch_env_args,
|
||||||
&mc_set_options,
|
&mc_set_options,
|
||||||
&wrapper,
|
&wrapper,
|
||||||
&memory,
|
&memory,
|
||||||
|
|||||||
@@ -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;
|
use tokio::process::Command;
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
|
pub(crate) mod hooks;
|
||||||
|
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod quick_play_version;
|
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
|
/// 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
|
/// game version. If the version isn't found in the cache, forces a manifest
|
||||||
/// refresh to pick up newly-released versions.
|
/// 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)
|
// Java options should be set in instance options (the existence of _JAVA_OPTIONS overwrites them)
|
||||||
command.env_remove("_JAVA_OPTIONS");
|
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
|
// Overwrites the minecraft options.txt file with the settings from the profile
|
||||||
// Uses 'a:b' syntax which is not quite yaml
|
// Uses 'a:b' syntax which is not quite yaml
|
||||||
@@ -1084,6 +1141,7 @@ pub async fn launch_minecraft(
|
|||||||
&instance.name,
|
&instance.name,
|
||||||
command,
|
command,
|
||||||
post_exit_hook,
|
post_exit_hook,
|
||||||
|
env_args,
|
||||||
state.directories.instance_logs_dir(&instance.path),
|
state.directories.instance_logs_dir(&instance.path),
|
||||||
version_info.logging.is_some(),
|
version_info.logging.is_some(),
|
||||||
main_class_keep_alive,
|
main_class_keep_alive,
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ impl ProcessManager {
|
|||||||
instance_name: &str,
|
instance_name: &str,
|
||||||
mut mc_command: Command,
|
mut mc_command: Command,
|
||||||
post_exit_command: Option<String>,
|
post_exit_command: Option<String>,
|
||||||
|
post_exit_env_vars: Vec<(String, String)>,
|
||||||
logs_folder: PathBuf,
|
logs_folder: PathBuf,
|
||||||
xml_logging: bool,
|
xml_logging: bool,
|
||||||
main_class_keep_alive: TempDir,
|
main_class_keep_alive: TempDir,
|
||||||
@@ -217,6 +218,7 @@ impl ProcessManager {
|
|||||||
instance_id.to_string(),
|
instance_id.to_string(),
|
||||||
instance_path.to_string(),
|
instance_path.to_string(),
|
||||||
post_exit_command,
|
post_exit_command,
|
||||||
|
post_exit_env_vars,
|
||||||
metadata.uuid,
|
metadata.uuid,
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -750,6 +752,7 @@ impl Process {
|
|||||||
instance_id: String,
|
instance_id: String,
|
||||||
instance_path: String,
|
instance_path: String,
|
||||||
post_exit_command: Option<String>,
|
post_exit_command: Option<String>,
|
||||||
|
post_exit_env_vars: Vec<(String, String)>,
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
async fn update_playtime(
|
async fn update_playtime(
|
||||||
@@ -885,7 +888,7 @@ impl Process {
|
|||||||
|
|
||||||
if let Some(command) = cmd.next() {
|
if let Some(command) = cmd.next() {
|
||||||
let mut command = Command::new(command);
|
let mut command = Command::new(command);
|
||||||
command.args(cmd).current_dir(
|
command.args(cmd).envs(post_exit_env_vars).current_dir(
|
||||||
state.directories.instances_dir().join(&instance_path),
|
state.directories.instances_dir().join(&instance_path),
|
||||||
);
|
);
|
||||||
command.spawn().map_err(IOError::from)?;
|
command.spawn().map_err(IOError::from)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user