Skip to content
Open
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
4 changes: 4 additions & 0 deletions crates/neovm-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ description = "Core runtime structures for NeoVM"
# auto-discovery off makes every remaining integration target explicit below.
autotests = false

[[test]]
name = "dump_environment"
path = "tests/dump_environment.rs"

[dependencies]
neovm-host-abi.workspace = true
neomacs-display-protocol.workspace = true
Expand Down
24 changes: 16 additions & 8 deletions crates/neovm-core/src/emacs_core/lisp/load/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6186,9 +6186,18 @@ pub fn create_bootstrap_evaluator_for_loadup(
maybe_trace_bootstrap_step("create_bootstrap_evaluator_with_features: enter");
let mut eval = super::eval::Context::new();
maybe_trace_bootstrap_step("create_bootstrap_evaluator_with_features: evaluator-new");
// Match GNU's initialization order: Lisp startup must inherit the host
// environment before loadup.el can inspect HOME or any other variable.
super::environment::install_host_environment_snapshot(&mut eval);
// GNU 31.1 emacs.c skips set_initial_environment when will_dump_p().
match invocation {
LoadupInvocation::PreloadOnly => {
super::environment::install_host_environment_snapshot(&mut eval);
super::shell_file_name::install(&mut eval);
}
LoadupInvocation::Dump(dump) => {
eval.loadup_dump_mode = Some(dump.mode());
// cus-start records this initial value in standard-value.
super::shell_file_name::install_dump_default(&mut eval);
}
}
let bootstrap_features = normalized_bootstrap_features(extra_features);
for feature in &bootstrap_features {
let _ = eval.provide_value(Value::symbol(feature), None);
Expand Down Expand Up @@ -6256,7 +6265,9 @@ pub fn create_bootstrap_evaluator_for_loadup(
Value::unibyte_string(lisp_directory_name_from_host_path(&project_root)),
);

// exec-path: list of dirs from PATH env var (C: callproc.c init_callproc_1)
// Keep build tools available until serialization: loadup queries Git
// before dumping. dump-emacs-portable omits the PATH derivative
// only while writing a loadup image; activation re-derives them.
let path_dirs: Vec<Value> = exec_path_dirs_from_env()
.into_iter()
.map(Value::unibyte_string)
Expand Down Expand Up @@ -6286,10 +6297,6 @@ pub fn create_bootstrap_evaluator_for_loadup(
eval.obarray.make_special(name);
}

// GNU callproc.c plus w32.c: inherited $SHELL, /bin/sh on POSIX,
// and the private cmdproxy.exe on Windows. Bootstrap and post-image
// startup share the same typed platform policy.
super::shell_file_name::install(&mut eval);
// shell-command-switch: GNU simple.el — defaults to "-c"
eval.set_variable("shell-command-switch", Value::unibyte_string("-c"));

Expand Down Expand Up @@ -6551,6 +6558,7 @@ fn activate_runtime_evaluator_at_root(
project_root: &Path,
role: RuntimeImageRole,
) -> Result<(), EvalError> {
eval.loadup_dump_mode = None;
finalize_cached_bootstrap_eval(eval, project_root, role).map_err(|error| {
tracing::error!("runtime evaluator activation failed: {error:?}");
error
Expand Down
16 changes: 16 additions & 0 deletions crates/neovm-core/src/emacs_core/lisp/native/builtins/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5814,7 +5814,23 @@ pub(crate) fn builtin_dump_emacs_portable(
}
ctx.set_variable("command-line-processed", Value::NIL);
ctx.set_variable("process-environment", Value::NIL);
// This global is needed by loadup's build tools, but runtime startup
// reconstructs it. Save it only in native locals, after the last GC:
// Lisp bindings retaining the originals would also enter the image.
// Ordinary runtime redumps keep GNU's behavior, regardless of filename.
let saved_build_exec_path = ctx.loadup_dump_mode.map(|_| {
let exec_path = ctx
.obarray()
.canonical_value_without_buffer(CanonicalAccess::DUMP, "exec-path")
.any_arm()
.unwrap_or(Value::NIL);
ctx.set_variable("exec-path", Value::NIL);
exec_path
});
let dump_result = crate::emacs_core::pdump::dump_to_file(ctx, dump_path);
if let Some(exec_path) = saved_build_exec_path {
ctx.set_variable("exec-path", exec_path);
}
ctx.set_variable("post-gc-hook", saved_post_gc_hook);
ctx.set_variable("command-line-processed", saved_command_line_processed);
ctx.set_variable("process-environment", saved_process_environment);
Expand Down
1 change: 1 addition & 0 deletions crates/neovm-core/src/emacs_core/runtime/eval/construct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2168,6 +2168,7 @@ impl Context {
minibuffer_selected_window: world.global.minibuffer_selected_window,
active_minibuffer_window: world.global.active_minibuffer_window,
shutdown_request: None,
loadup_dump_mode: None,
shutdown_in_progress: false,
daemon: world.global.daemon,
_reserved_input_mode_interrupt: true,
Expand Down
10 changes: 10 additions & 0 deletions crates/neovm-core/src/emacs_core/runtime/eval/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3580,10 +3580,20 @@ pub struct Context {
macro_perf_enabled: bool,
/// Custom variable manager — defcustom/defgroup system.
pub(crate) custom: CustomManager,
/// Native construction state, never serialized. Unlike an ordinary
/// runtime redump, loadup must omit build-only environment derivatives.
/// Use trailing padding to preserve the pinned Context field offsets.
pub(crate) loadup_dump_mode: Option<super::load::LoadupDumpMode>,
}

// A mutex does not make the host hooks, Rc leases or TLS views transferable.
static_assertions::assert_not_impl_any!(Context: Send, Sync);
// Construction state must fit the single trailing byte without growing
// Context or shifting its pinned native/JIT ABI.
static_assertions::const_assert_eq!(
std::mem::size_of::<Option<super::load::LoadupDumpMode>>(),
1
);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ShutdownRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ impl Context {
minibuffer_selected_window: world.global.minibuffer_selected_window,
active_minibuffer_window: world.global.active_minibuffer_window,
shutdown_request: None,
loadup_dump_mode: None,
shutdown_in_progress: false,
daemon: world.global.daemon,
_reserved_input_mode_interrupt: true,
Expand Down
7 changes: 4 additions & 3 deletions crates/neovm-core/src/emacs_core/system/environment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ fn host_process_environment() -> Value {
/// Install the environment inherited by this Neomacs process as the Lisp
/// startup environment.
///
/// GNU initializes `process-environment` before `loadup.el`, so every startup
/// consumer observes the same HOME, PATH, and other host variables as file
/// expansion and subprocess creation. A cached evaluator needs the same
/// GNU initializes `process-environment` before Lisp startup, except when
/// constructing a dump (`!will_dump_p()` in emacs.c). Runtime consumers
/// observe the same HOME, PATH, and other host variables as file expansion
/// and subprocess creation. A cached evaluator needs the same
/// operation when it is activated, because its dumped environment belongs to
/// the process that created the cache rather than the current process.
pub(crate) fn install_host_environment_snapshot(eval: &mut Context) {
Expand Down
11 changes: 11 additions & 0 deletions crates/neovm-core/src/emacs_core/system/shell_file_name/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ pub(crate) fn resolve_current() -> ResolvedShellFileName {
/// Install the one authoritative startup answer into Lisp.
pub(crate) fn install(eval: &mut Context) {
let shell = resolve_current();
install_resolved(eval, shell);
}

/// Image construction must not retain the builder's SHELL or executable
/// search directory. Runtime startup replaces this portable default with
/// the inherited shell or the installed private cmdproxy path.
pub(crate) fn install_dump_default(eval: &mut Context) {
install_resolved(eval, resolve_for(CURRENT_PLATFORM, None, None));
}

fn install_resolved(eval: &mut Context, shell: ResolvedShellFileName) {
eval.set_variable("shell-file-name", Value::unibyte_string(shell.lisp_name()));
eval.obarray.make_special("shell-file-name");
}
Expand Down
195 changes: 195 additions & 0 deletions crates/neovm-core/tests/dump_environment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//! Build images exclude host state; activation supplies the runtime environment.
use neovm_core::emacs_core::{
format_eval_result,
load::{
LoadupDumpInvocation, LoadupDumpMode, LoadupInvocation, RuntimeImageRole,
activate_runtime_evaluator, create_bootstrap_evaluator_for_loadup,
},
pdump::load_from_dump,
};
use std::{fs, path::PathBuf, process::Command};
use tempfile::tempdir;

#[test]
fn production_dump_does_not_capture_the_build_environment() {
assert_dump_environment(
LoadupDumpMode::Pdump,
"production_dump_does_not_capture_the_build_environment",
);
}

#[test]
fn bootstrap_dump_does_not_capture_the_build_environment() {
assert_dump_environment(
LoadupDumpMode::Pbootstrap,
"bootstrap_dump_does_not_capture_the_build_environment",
);
}

fn assert_dump_environment(mode: LoadupDumpMode, test_name: &str) {
const MARKER: &str = "/nix/store/00000000000000000000000000000000-issue578-build-only";
const CHILD: &str = "NEOMACS_DUMP_ENVIRONMENT_TEST_CHILD";
let (dump_name, role) = match mode {
LoadupDumpMode::Pdump => ("neomacs.pdump", RuntimeImageRole::Final),
LoadupDumpMode::Pbootstrap => ("bootstrap-neomacs.pdump", RuntimeImageRole::Bootstrap),
};
if std::env::var_os(CHILD).is_none() {
// Context initializes invocation-directory from the executable. Run
// the real dump path in a private process and directory, without
// mutating this test process's environment or build artifacts.
let directory = tempdir().unwrap();
let executable = directory
.path()
.join(format!("neomacs-temacs{}", std::env::consts::EXE_SUFFIX));
fs::copy(std::env::current_exe().unwrap(), &executable).unwrap();
fs::write(
directory
.path()
.join(format!("neomacs{}", std::env::consts::EXE_SUFFIX)),
b"dump fixture",
)
.unwrap();
let output = Command::new(&executable)
.args(["--exact", test_name, "--nocapture"])
.env(CHILD, "dump")
.env("NEOMACS_ISSUE578_BUILD_ONLY", MARKER)
.env(
"PATH",
std::env::join_paths(
std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default())
.chain(std::iter::once(PathBuf::from(MARKER))),
)
.unwrap(),
)
.env("SHELL", format!("{MARKER}/bin/sh"))
.output()
.expect("run isolated dump construction");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);

let bytes = fs::read(directory.path().join(dump_name)).unwrap();
assert!(
!bytes
.windows(MARKER.len())
.any(|window| window == MARKER.as_bytes()),
"{dump_name} retains a build-only environment value",
);
let runtime = Command::new(&executable)
.args(["--exact", test_name, "--nocapture"])
.env(CHILD, "runtime")
.env_remove("NEOMACS_ISSUE578_BUILD_ONLY")
.env("NEOMACS_ISSUE578_RUNTIME", "runtime")
.env("NEOMACS_ISSUE578_RUNTIME_PATH", "issue578-runtime-bin")
.env(
"PATH",
std::env::join_paths(
std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default())
.chain(std::iter::once(PathBuf::from("issue578-runtime-bin"))),
)
.unwrap(),
)
.env("SHELL", "issue578-runtime-shell")
.output()
.expect("activate the image in a different runtime environment");
assert!(
runtime.status.success(),
"{}",
String::from_utf8_lossy(&runtime.stderr)
);
return;
}
let temacs = std::env::current_exe().unwrap();
let dump_path = temacs.parent().unwrap().join(dump_name);
if std::env::var(CHILD).as_deref() == Ok("dump") {
let invocation = LoadupInvocation::Dump(LoadupDumpInvocation::new(
mode,
vec![
temacs.to_string_lossy().into_owned(),
"--batch".into(),
"-l".into(),
"loadup".into(),
format!("--temacs={}", mode.as_gnu_string()),
],
));
let eval = create_bootstrap_evaluator_for_loadup(&[], &invocation)
.expect("build the dump through loadup");
assert_eq!(
eval.shutdown_request().map(|request| request.exit_code),
Some(0)
);
drop(eval);
let mut restored = load_from_dump(&dump_path).expect("restore the dump");

if mode == LoadupDumpMode::Pdump {
let version = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."))
.output();
if let Ok(version) = version
&& version.status.success()
{
let expected = String::from_utf8(version.stdout).unwrap();
assert_eq!(
format_eval_result(&restored.eval_str("emacs-repository-version")),
format!("OK {:?}", expected.trim()),
"loadup must still be able to query Git before serializing",
);
}
}
// GNU 31.1 emacs.c guards set_initial_environment with !will_dump_p().
assert_eq!(
format_eval_result(
&restored.eval_str("(and (null initial-environment) (null process-environment))",)
),
"OK t"
);
} else {
let mut restored = load_from_dump(&dump_path).expect("restore the dump");
activate_runtime_evaluator(&mut restored, role).expect("activate the runtime environment");
assert_eq!(
format_eval_result(&restored.eval_str(
r#"
(and (equal (getenv "NEOMACS_ISSUE578_RUNTIME") "runtime")
(null (getenv "NEOMACS_ISSUE578_BUILD_ONLY"))
(not (eq initial-environment process-environment))
(member (getenv "NEOMACS_ISSUE578_RUNTIME_PATH") exec-path)
(equal shell-file-name "issue578-runtime-shell")
(equal (getenv-internal "NEOMACS_ISSUE578_RUNTIME" initial-environment)
"runtime"))
"#
)),
"OK t"
);
#[cfg(unix)]
assert_eq!(
format_eval_result(&restored.eval_str(
r#"
(with-temp-buffer
(list (call-process "/bin/sh" nil t nil "-c" "printf %s \"$NEOMACS_ISSUE578_RUNTIME\"")
(buffer-string)))
"#
)),
"OK (0 \"runtime\")"
);
// A restored runtime must not inherit the producer's construction
// state: ordinary user redumps preserve their shell and search path.
let redump = dump_path.with_file_name("runtime-redump.pdump");
let form = format!(
"(progn (setq noninteractive t) (dump-emacs-portable {:?}))",
redump.to_string_lossy(),
);
assert_eq!(format_eval_result(&restored.eval_str(&form)), "OK nil");
drop(restored);
let mut redumped = load_from_dump(&redump).expect("restore an ordinary runtime redump");
assert_eq!(
format_eval_result(&redumped.eval_str(
r#"(and (equal shell-file-name "issue578-runtime-shell")
(member "issue578-runtime-bin" exec-path) t)"#,
)),
"OK t",
);
}
}
17 changes: 16 additions & 1 deletion nix/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ let
gstreamerRuntime.pluginScanner
]
++
lib.optionals (pkgs.stdenv.hostPlatform.isLinux && builtins.elem "webview" productionCapabilities.cargoFeatures)
lib.optionals
(pkgs.stdenv.hostPlatform.isLinux && builtins.elem "webview" productionCapabilities.cargoFeatures)
[
"--set-default"
"WPE_BACKEND_LIBRARY"
Expand Down Expand Up @@ -107,6 +108,20 @@ craneLib.buildPackage (
src = source;
inherit cargoArtifacts;

# A dumped build environment can turn build-only inputs into runtime
# dependencies even when the executable never links them. Let Nix's
# output reference scan reject that regression after fixup hooks run.
disallowedReferences = [
rustToolchain
cargoArtifacts
cargoSrc
source
pkgs.stdenv.cc
pkgs.llvmPackages.clang
pkgs.pkg-config
pkgs.binutils
];

postBuild = ''
cargo xtask fresh-build --release --skip-build
'';
Expand Down
Loading