diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md
index 863dbcbca..b8318b281 100644
--- a/crates/socket-patch-cli/CLI_CONTRACT.md
+++ b/crates/socket-patch-cli/CLI_CONTRACT.md
@@ -1143,7 +1143,7 @@ Env-only knobs used for hosted upstream restoration and JVM metadata verificatio
| Env var | Default | Notes |
|---|---|---|
| `SOCKET_NPM_REGISTRY` | `https://registry.npmjs.org` | Base for npm version documents (`//`, a scoped name's `/` as `%2f`; `dist.tarball` / `integrity` / `shasum`) the npm-family and vlt upstream restore reads, unless the project names another registry (yarn berry `npmRegistryServer`, pnpm's lock-sibling `.npmrc` `registry` / `@scope:registry` or pnpm-workspace.yaml `registry` / `registries` as the pnpm major reads them, vlt's node registry), whose document is read first. |
-| `SOCKET_GOPROXY` | `https://proxy.golang.org` | Go module proxy used for upstream restoration, honoring `GOPROXY`, `GONOPROXY` and `GOPRIVATE`. |
+| `SOCKET_GOPROXY` | `https://proxy.golang.org` | Go module proxy used for upstream restoration, honoring `GOPROXY`, `GONOPROXY` and `GOPRIVATE`. Like every Go setting socket-patch reads (`GOMODCACHE`, `GOPATH`, `GOWORK`, `GOSUMDB`, `GONOSUMDB`), each resolves as go resolves it: the environment, then the `go env -w` file (`$GOENV`, default `os.UserConfigDir()/go/env`; `GOENV=off` disables it), then go's default. |
| `SOCKET_MAVEN_REGISTRY` | `https://repo1.maven.org/maven2` | maven2 base for the fallback upstream-pom download. |
| `SOCKET_CRATES_INDEX` | `https://index.crates.io` | v5.0 upstream restore: the crates.io sparse index whose `checksum` a restored `Cargo.lock` entry gets back. |
| `SOCKET_GOSUMDB_URL` | `https://sum.golang.org` | v5.0 upstream restore: the checksum database the restored go.sum lines are checked against. Without it, `GOSUMDB=off` or a module matching `GONOSUMDB` (default `GOPRIVATE`) skips the database and the hashes come from the module proxy's bytes alone (`SOCKET_GOPROXY` above). |
diff --git a/crates/socket-patch-cli/tests/e2e_golang.rs b/crates/socket-patch-cli/tests/e2e_golang.rs
index 095a5d5b5..e2d49c309 100644
--- a/crates/socket-patch-cli/tests/e2e_golang.rs
+++ b/crates/socket-patch-cli/tests/e2e_golang.rs
@@ -282,6 +282,83 @@ async fn scan_discovers_go_modules() {
);
}
+/// #344: a module cache configured with `go env -w GOMODCACHE=…` (the
+/// `$GOENV` file, not the environment) is the one scanned, exactly as
+/// `go env GOMODCACHE` and `go build` resolve it.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn scan_finds_the_cache_named_by_the_go_env_file() {
+ let server = MockServer::start().await;
+ mount_batch(&server).await;
+ let api_url = server.uri();
+
+ let dir = tempfile::tempdir().unwrap();
+ let cache_dir = dir.path().join("configured-cache");
+ let gin_dir = cache_dir.join("github.com/gin-gonic/gin@v1.9.1");
+ std::fs::create_dir_all(&gin_dir).unwrap();
+ std::fs::write(
+ gin_dir.join("go.mod"),
+ "module github.com/gin-gonic/gin\n\ngo 1.21\n",
+ )
+ .unwrap();
+ let project = dir.path().join("project");
+ std::fs::create_dir_all(&project).unwrap();
+ std::fs::write(
+ project.join("go.mod"),
+ "module example.com/myproject\n\ngo 1.21\n",
+ )
+ .unwrap();
+ // What `go env -w GOMODCACHE=` writes.
+ let goenv = dir.path().join("goenv");
+ std::fs::write(&goenv, format!("GOMODCACHE={}\n", cache_dir.display())).unwrap();
+ // HOME points at an empty dir so the `$HOME/go/pkg/mod` default finds
+ // nothing.
+ let home = dir.path().join("home");
+ std::fs::create_dir_all(&home).unwrap();
+
+ let (project2, api) = (project.clone(), api_url.clone());
+ let output = tokio::task::spawn_blocking(move || {
+ common::hermetic::command(&binary())
+ .args(["scan", "--json", "--cwd", project2.to_str().unwrap()])
+ .current_dir(&project2)
+ .env("GOENV", &goenv)
+ .env("HOME", &home)
+ .env("USERPROFILE", &home)
+ .env("SOCKET_API_URL", &api)
+ .env("SOCKET_API_TOKEN", "sktsec_dummy_e2e_golang_token_api")
+ .env("SOCKET_ORG_SLUG", ORG)
+ .env_remove("GOMODCACHE")
+ .env_remove("GOPATH")
+ .env_remove("SOCKET_ECOSYSTEMS")
+ .env_remove("SOCKET_GLOBAL")
+ .env_remove("SOCKET_GLOBAL_PREFIX")
+ .env_remove("SOCKET_JSON")
+ .env_remove("SOCKET_SILENT")
+ .env_remove("SOCKET_OFFLINE")
+ .env_remove("SOCKET_PROXY_URL")
+ .env_remove("SOCKET_BATCH_SIZE")
+ .output()
+ .expect("Failed to run socket-patch binary")
+ })
+ .await
+ .unwrap();
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let json: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|e| {
+ panic!(
+ "scan --json must emit JSON ({e}):\n{stdout}\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ )
+ });
+ assert_eq!(
+ json["scannedPackages"], 1,
+ "the go env -w GOMODCACHE cache must be crawled; got:\n{json:#}"
+ );
+ let purls = batched_purls(&server).await;
+ assert_eq!(
+ purls,
+ BTreeSet::from(["pkg:golang/github.com/gin-gonic/gin@v1.9.1".to_string()])
+ );
+}
+
/// Verify `socket-patch scan` discovers AND case-decodes Go modules.
///
/// Go's module cache stores uppercase letters as `!`+lowercase, so
diff --git a/crates/socket-patch-core/src/crawlers/go_crawler.rs b/crates/socket-patch-core/src/crawlers/go_crawler.rs
index 439b77ab4..4451b88e1 100644
--- a/crates/socket-patch-core/src/crawlers/go_crawler.rs
+++ b/crates/socket-patch-core/src/crawlers/go_crawler.rs
@@ -175,14 +175,14 @@ impl GoCrawler {
// ------------------------------------------------------------------
/// Get `GOMODCACHE`, falling back to `$GOPATH/pkg/mod/` or `$HOME/go/pkg/mod/`.
+ /// Each setting resolves as go resolves it: the environment, then the
+ /// `go env -w` file ([`crate::utils::go_env`]), so a cache configured
+ /// with `go env -w GOMODCACHE=…` is the one crawled (#344).
fn get_gomodcache() -> Option {
- if let Ok(cache) = std::env::var("GOMODCACHE") {
- let p = PathBuf::from(cache);
- if !p.as_os_str().is_empty() {
- return Some(p);
- }
+ if let Some(cache) = crate::utils::go_env::go_env("GOMODCACHE") {
+ return Some(PathBuf::from(cache));
}
- if let Ok(gopath) = std::env::var("GOPATH") {
+ if let Some(gopath) = crate::utils::go_env::go_env("GOPATH") {
// GOPATH may list several directories separated by the OS path
// separator (`:` on Unix, `;` on Windows). Go uses the FIRST
// entry for the module cache, so split rather than treating the
@@ -314,12 +314,13 @@ fn go_sum_scope(cwd: &Path) -> Option> {
/// Whether the go command would build `cwd` in workspace mode: `GOWORK`
/// names a file, or (`GOWORK` unset or empty) a `go.work` exists in `cwd`
-/// or an ancestor. `GOWORK=off` disables workspaces.
+/// or an ancestor. `GOWORK=off` disables workspaces. `GOWORK` resolves
+/// from the environment, then the `go env -w` file.
fn workspace_in_effect(cwd: &Path) -> bool {
- match std::env::var_os("GOWORK") {
+ match crate::utils::go_env::go_env("GOWORK") {
Some(v) if v == "off" => false,
- Some(v) if !v.is_empty() => true,
- _ => go_work_at_or_above(cwd, &std::env::current_dir().unwrap_or_default()),
+ Some(_) => true,
+ None => go_work_at_or_above(cwd, &std::env::current_dir().unwrap_or_default()),
}
}
@@ -1565,6 +1566,8 @@ mod tests {
// own project. Twin of `m2_repo_path`'s / `nuget_home`'s guards.
let gopath_a = tempfile::tempdir().unwrap();
let home_b = tempfile::tempdir().unwrap();
+ // No `go env -w` file: the developer's own must not leak in.
+ let _goenv = EnvGuard::set("GOENV", "off");
// Case A: empty GOMODCACHE falls through to GOPATH, and the empty
// FIRST GOPATH entry is skipped in favor of the next non-empty one
@@ -1602,6 +1605,33 @@ mod tests {
);
}
+ /// #344: GOMODCACHE / GOPATH written with `go env -w` (the `$GOENV`
+ /// file) locate the cache go itself uses; the environment still wins.
+ #[test]
+ #[serial_test::serial]
+ fn test_get_gomodcache_reads_the_go_env_file() {
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("env");
+ let cache = dir.path().join("custom-cache");
+ let gopath = dir.path().join("gopath");
+ let _gomodcache = EnvGuard::unset("GOMODCACHE");
+ let _gopath = EnvGuard::unset("GOPATH");
+ let _goenv = EnvGuard::set("GOENV", file.to_str().unwrap());
+
+ std::fs::write(&file, format!("GOMODCACHE={}\n", cache.display())).unwrap();
+ assert_eq!(GoCrawler::get_gomodcache(), Some(cache.clone()));
+
+ std::fs::write(&file, format!("GOPATH={}\n", gopath.display())).unwrap();
+ assert_eq!(
+ GoCrawler::get_gomodcache(),
+ Some(gopath.join("pkg").join("mod"))
+ );
+
+ let env_cache = dir.path().join("env-cache");
+ let _env = EnvGuard::set("GOMODCACHE", env_cache.to_str().unwrap());
+ assert_eq!(GoCrawler::get_gomodcache(), Some(env_cache));
+ }
+
#[tokio::test]
async fn test_parse_versioned_dir_second_visit_same_purl_returns_none() {
// The `seen` dedup contract: crawl_all threads one HashSet through
diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/client.rs b/crates/socket-patch-core/src/patch/redirect/upstream/client.rs
index 1c055b800..cc5a68d9a 100644
--- a/crates/socket-patch-core/src/patch/redirect/upstream/client.rs
+++ b/crates/socket-patch-core/src/patch/redirect/upstream/client.rs
@@ -691,7 +691,9 @@ pub(crate) const DEFAULT_GOSUMDB: &str = "https://sum.golang.org";
/// The checksum database go would consult for `module`, or `None` when go
/// would not (`GOSUMDB=off`, or the module matches `GONOSUMDB` /
/// `GOPRIVATE`) — the hashes are then computed from the module proxy's
-/// bytes instead. An explicit `SOCKET_GOSUMDB_URL` always wins.
+/// bytes instead. An explicit `SOCKET_GOSUMDB_URL` always wins. The Go
+/// settings resolve from the environment, then the `go env -w` file
+/// ([`crate::utils::go_env`]).
fn gosumdb_base(module: &str) -> Option {
if let Ok(v) = std::env::var("SOCKET_GOSUMDB_URL") {
let v = v.trim().trim_end_matches('/').to_string();
@@ -699,7 +701,7 @@ fn gosumdb_base(module: &str) -> Option {
return Some(v);
}
}
- let nonempty = |key: &str| std::env::var(key).ok().filter(|v| !v.trim().is_empty());
+ let nonempty = |key: &str| crate::utils::go_env::go_env(key).filter(|v| !v.trim().is_empty());
if nonempty("GOSUMDB").is_some_and(|v| v.trim() == "off") {
return None;
}
diff --git a/crates/socket-patch-core/src/utils/go_env.rs b/crates/socket-patch-core/src/utils/go_env.rs
new file mode 100644
index 000000000..73955a4b1
--- /dev/null
+++ b/crates/socket-patch-core/src/utils/go_env.rs
@@ -0,0 +1,183 @@
+//! Go configuration lookup the way the `go` command resolves it (#344).
+//!
+//! go reads each setting from the process environment first, then from the
+//! per-user Go environment file that `go env -w` writes, then falls back to
+//! its built-in default. The Go docs tell users to configure `GOPRIVATE`,
+//! `GOPROXY` and `GOMODCACHE` with `go env -w`, so consulting only the
+//! environment silently ignores those settings: a private module path is
+//! requested from `proxy.golang.org`, a corporate mirror is bypassed, and a
+//! relocated module cache is not found.
+//!
+//! The file is `$GOENV` when set (`GOENV=off` disables it), else
+//! `os.UserConfigDir()/go/env`: `$XDG_CONFIG_HOME/go/env` (or
+//! `~/.config/go/env`) on Linux and other Unix, `~/Library/Application
+//! Support/go/env` on macOS, `%AppData%\go\env` on Windows.
+//!
+//! Out of scope: `$GOROOT/go.env` (Go 1.21+), the toolchain's own lowest
+//! layer. Upstream Go ships it with the same values as the built-in
+//! defaults, and locating it would mean running `go`.
+
+use std::path::PathBuf;
+
+/// The effective value of Go setting `key`: a non-empty environment variable
+/// wins, else the Go environment file's non-empty value, else `None` (the
+/// caller applies go's default). An empty environment variable falls
+/// through to the file, as go's own `cfg.Getenv` does.
+pub(crate) fn go_env(key: &str) -> Option {
+ go_env_with(key, |k| std::env::var(k).ok())
+}
+
+/// [`go_env`] over an injected environment lookup.
+pub(crate) fn go_env_with(key: &str, env: impl Fn(&str) -> Option) -> Option {
+ if let Some(v) = env(key).filter(|v| !v.is_empty()) {
+ return Some(v);
+ }
+ let file = go_env_file(&env)?;
+ let text = crate::utils::fs::read_regular_to_string_sync(&file).ok()?;
+ lookup_in_env_file(&text, key).filter(|v| !v.is_empty())
+}
+
+/// The Go environment file go would read, or `None` when `GOENV=off` or no
+/// user config directory resolves.
+fn go_env_file(env: &impl Fn(&str) -> Option) -> Option {
+ match env("GOENV").filter(|v| !v.is_empty()) {
+ Some(v) if v == "off" => None,
+ Some(v) => Some(PathBuf::from(v)),
+ None => Some(user_config_dir(env)?.join("go").join("env")),
+ }
+}
+
+/// Go's `os.UserConfigDir`.
+fn user_config_dir(env: &impl Fn(&str) -> Option) -> Option {
+ let absolute = |v: Option| {
+ v.filter(|v| !v.is_empty())
+ .map(PathBuf::from)
+ .filter(|p| p.is_absolute())
+ };
+ if cfg!(windows) {
+ return absolute(env("AppData").or_else(|| env("APPDATA")));
+ }
+ let home = || absolute(env("HOME"));
+ if cfg!(target_os = "macos") {
+ return Some(home()?.join("Library").join("Application Support"));
+ }
+ match env("XDG_CONFIG_HOME").filter(|v| !v.is_empty()) {
+ // go refuses a relative XDG_CONFIG_HOME rather than falling back.
+ Some(xdg) => absolute(Some(xdg)),
+ None => Some(home()?.join(".config")),
+ }
+}
+
+/// `key`'s value in a Go environment file (`cmd/go/internal/cfg`
+/// `readEnvFile`): one `KEY=VALUE` per line, the value taken verbatim after
+/// the first `=`; a line not starting with an uppercase letter (blank, a
+/// `#` comment) or without `=` is skipped. A later line for the same key
+/// wins.
+fn lookup_in_env_file(text: &str, key: &str) -> Option {
+ text.lines()
+ .map(|line| line.strip_suffix('\r').unwrap_or(line))
+ .filter(|line| line.starts_with(|c: char| c.is_ascii_uppercase()))
+ .filter_map(|line| line.split_once('='))
+ .rfind(|(k, _)| *k == key)
+ .map(|(_, v)| v.to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::collections::HashMap;
+
+ fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option {
+ let map: HashMap = pairs
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+ move |k: &str| map.get(k).cloned()
+ }
+
+ #[test]
+ fn parses_go_env_w_output() {
+ let text = "# comment\nGOPROXY=http://127.0.0.1:18702\r\nGOPRIVATE=example.com,*.corp\n\
+ lower=x\nNOEQUALS\nGOFLAGS=-mod=mod -tags=a=b\nGOPROXY=https://mirror\n";
+ assert_eq!(
+ lookup_in_env_file(text, "GOPROXY").as_deref(),
+ Some("https://mirror")
+ );
+ assert_eq!(
+ lookup_in_env_file(text, "GOPRIVATE").as_deref(),
+ Some("example.com,*.corp")
+ );
+ assert_eq!(
+ lookup_in_env_file(text, "GOFLAGS").as_deref(),
+ Some("-mod=mod -tags=a=b")
+ );
+ assert_eq!(lookup_in_env_file(text, "lower"), None);
+ assert_eq!(lookup_in_env_file(text, "NOEQUALS"), None);
+ assert_eq!(lookup_in_env_file(text, "GONOPROXY"), None);
+ }
+
+ /// env (non-empty) → `$GOENV` file → `None`; an empty env var falls
+ /// through to the file and `GOENV=off` disables it.
+ #[test]
+ fn env_then_goenv_file_then_default() {
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("env");
+ std::fs::write(&file, "GOPRIVATE=example.com\nGOMODCACHE=/cache\n").unwrap();
+ let goenv = file.to_str().unwrap();
+
+ let env = env_of(&[("GOENV", goenv)]);
+ assert_eq!(
+ go_env_with("GOPRIVATE", &env).as_deref(),
+ Some("example.com")
+ );
+ assert_eq!(go_env_with("GOPROXY", &env), None);
+
+ let env = env_of(&[("GOENV", goenv), ("GOPRIVATE", "env.example")]);
+ assert_eq!(
+ go_env_with("GOPRIVATE", &env).as_deref(),
+ Some("env.example")
+ );
+ let env = env_of(&[("GOENV", goenv), ("GOPRIVATE", "")]);
+ assert_eq!(
+ go_env_with("GOPRIVATE", &env).as_deref(),
+ Some("example.com")
+ );
+
+ let env = env_of(&[("GOENV", "off"), ("HOME", "/nonexistent")]);
+ assert_eq!(go_env_with("GOPRIVATE", &env), None);
+ let env = env_of(&[("GOENV", dir.path().join("missing").to_str().unwrap())]);
+ assert_eq!(go_env_with("GOPRIVATE", &env), None);
+ }
+
+ /// Without `GOENV` the file is `os.UserConfigDir()/go/env`.
+ #[test]
+ fn default_file_is_under_the_user_config_dir() {
+ let dir = tempfile::tempdir().unwrap();
+ let base = dir.path();
+ let config = if cfg!(windows) {
+ base.join("roaming")
+ } else if cfg!(target_os = "macos") {
+ base.join("Library").join("Application Support")
+ } else {
+ base.join("xdg")
+ };
+ std::fs::create_dir_all(config.join("go")).unwrap();
+ std::fs::write(config.join("go").join("env"), "GOPROXY=https://mirror\n").unwrap();
+ let env = env_of(&[
+ ("HOME", base.to_str().unwrap()),
+ ("XDG_CONFIG_HOME", base.join("xdg").to_str().unwrap()),
+ ("AppData", base.join("roaming").to_str().unwrap()),
+ ]);
+ assert_eq!(
+ go_env_with("GOPROXY", &env).as_deref(),
+ Some("https://mirror")
+ );
+ // A relative XDG_CONFIG_HOME / HOME resolves nothing.
+ let env = env_of(&[
+ ("HOME", "rel"),
+ ("XDG_CONFIG_HOME", "rel"),
+ ("AppData", "rel"),
+ ]);
+ assert_eq!(go_env_with("GOPROXY", &env), None);
+ }
+}
diff --git a/crates/socket-patch-core/src/utils/mod.rs b/crates/socket-patch-core/src/utils/mod.rs
index cf4d79805..ea204bc69 100644
--- a/crates/socket-patch-core/src/utils/mod.rs
+++ b/crates/socket-patch-core/src/utils/mod.rs
@@ -7,6 +7,7 @@ pub(crate) mod durability;
pub mod env_compat;
pub mod failpoint;
pub mod fs;
+pub(crate) mod go_env;
pub mod group_commit;
pub(crate) mod http;
pub(crate) mod line_endings;
diff --git a/crates/socket-patch-core/src/vendor/registry_fetch.rs b/crates/socket-patch-core/src/vendor/registry_fetch.rs
index dc3f3cbd9..7c6eb9b69 100644
--- a/crates/socket-patch-core/src/vendor/registry_fetch.rs
+++ b/crates/socket-patch-core/src/vendor/registry_fetch.rs
@@ -851,6 +851,11 @@ pub const DEFAULT_GOPROXY: &str = "https://proxy.golang.org";
/// the module matches GONOPROXY (defaulting to GOPRIVATE). Falling back to a
/// public proxy there would send a private module path off the machine.
/// A non-empty `SOCKET_GOPROXY` is an explicit choice and always wins.
+///
+/// GOPROXY / GONOPROXY / GOPRIVATE resolve as go resolves them: the
+/// environment, then the `go env -w` file ([`crate::utils::go_env`]), so a
+/// GOPRIVATE set with `go env -w` keeps a private path off the public
+/// proxy and a mirror set that way is used (#344).
pub(crate) fn goproxy_base(module: &str) -> Result {
if let Ok(v) = std::env::var("SOCKET_GOPROXY") {
let v = v.trim_end_matches('/').to_string();
@@ -858,7 +863,7 @@ pub(crate) fn goproxy_base(module: &str) -> Result {
return Ok(v);
}
}
- let nonempty = |key: &str| std::env::var(key).ok().filter(|v| !v.trim().is_empty());
+ let nonempty = |key: &str| crate::utils::go_env::go_env(key).filter(|v| !v.trim().is_empty());
if let Some((key, patterns)) = nonempty("GONOPROXY")
.map(|v| ("GONOPROXY", v))
.or_else(|| nonempty("GOPRIVATE").map(|v| ("GOPRIVATE", v)))
@@ -1979,12 +1984,61 @@ mod tests {
);
}
+ /// #344: GOPROXY / GOPRIVATE / GONOPROXY written with `go env -w` (the
+ /// `$GOENV` file) are honored like the environment variables: a private
+ /// module is refused rather than requested from the public proxy, and a
+ /// mirror is used. The environment still wins over the file.
+ #[test]
+ #[serial_test::serial]
+ fn goproxy_base_reads_the_go_env_file() {
+ let keys = [
+ "GOENV",
+ "GOPROXY",
+ "GOPRIVATE",
+ "GONOPROXY",
+ "SOCKET_GOPROXY",
+ ];
+ let saved: Vec<_> = keys.iter().map(|k| (*k, std::env::var(k).ok())).collect();
+ let dir = tempfile::tempdir().unwrap();
+ let file = dir.path().join("env");
+ std::fs::write(
+ &file,
+ "GOPROXY=http://127.0.0.1:18702\nGOPRIVATE=example.com\n",
+ )
+ .unwrap();
+ for k in &keys[1..] {
+ std::env::remove_var(k);
+ }
+ std::env::set_var("GOENV", &file);
+ let private = goproxy_base("example.com/upstream");
+ let public = goproxy_base("golang.org/x/text");
+ std::env::set_var("GOPROXY", "https://env.example");
+ let env_wins = goproxy_base("golang.org/x/text");
+ std::env::set_var("GOENV", "off");
+ std::env::remove_var("GOPROXY");
+ let off = goproxy_base("example.com/upstream");
+ for (k, v) in saved {
+ match v {
+ Some(v) => std::env::set_var(k, v),
+ None => std::env::remove_var(k),
+ }
+ }
+ let err = private.unwrap_err();
+ assert!(err.contains("matches GOPRIVATE"), "{err}");
+ assert_eq!(public.as_deref(), Ok("http://127.0.0.1:18702"));
+ assert_eq!(env_wins.as_deref(), Ok("https://env.example"));
+ assert_eq!(off.as_deref(), Ok(DEFAULT_GOPROXY));
+ }
+
#[test]
#[serial_test::serial]
fn goproxy_base_env_precedence() {
const MODULE: &str = "example.com/m";
let saved_socket = std::env::var("SOCKET_GOPROXY").ok();
let saved = std::env::var("GOPROXY").ok();
+ // No `go env -w` file: the developer's own must not leak in.
+ let saved_goenv = std::env::var("GOENV").ok();
+ std::env::set_var("GOENV", "off");
// SOCKET_GOPROXY wins over GOPROXY (trailing slash trimmed).
std::env::set_var("SOCKET_GOPROXY", "https://socket.example/");
@@ -2011,6 +2065,10 @@ mod tests {
Some(v) => std::env::set_var("GOPROXY", v),
None => std::env::remove_var("GOPROXY"),
}
+ match saved_goenv {
+ Some(v) => std::env::set_var("GOENV", v),
+ None => std::env::remove_var("GOENV"),
+ }
assert_eq!(socket_wins.as_deref(), Ok("https://socket.example"));
assert_eq!(
empty_falls_through.as_deref(),