Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os/exec: improved API and default behavior for the PWD env variable #50599

Closed
bcmills opened this issue Jan 13, 2022 · 16 comments
Closed

os/exec: improved API and default behavior for the PWD env variable #50599

bcmills opened this issue Jan 13, 2022 · 16 comments

Comments

@bcmills
Copy link
Contributor

bcmills commented Jan 13, 2022

Background

On POSIX platforms, the PWD environment variable represents (emphasis mine) “an absolute pathname of the current working directory.”

The specific value of PWD is important if the current working directory can be reached through multiple absolute paths, as is the case for subdirectories of /tmp on macOS. It affects the behavior of os.Getwd (see #8400), which can in turn affect both the error and non-error output of programs, including behaviors like paths in debug metadata (which can invalidate build caches if skewed) and parent-directory lookup (important for, among other things, identifying the current Go workspace and module). A PWD in /tmp arises naturally in any Go test that executes a subprocesses within a directory obtained by calling (*testing.T).TempDir, which is (or ought to be!) common for tests of command-line binaries.

Failing to set PWD appropriately when setting the Dir has led to a number of subtle bugs within the Go project, such as:

Moreover, setting PWD correctly is not trivial: PWD is required to be absolute, but the Dir field may be relative, leading to bugs like #46832 when the two do not match.

Proposal

I propose two ergonomic improvements to the exec.Cmd API:

  1. If the Env field of an exec.Cmd is nil and its Dir field is non-empty on a platform that uses PWD, the subprocess's PWD variable should be implicitly set to an appropriate absolute path (computed from os.Getwd() if Dir is relative) when the command is started.

    • If the Dir field is empty, the PWD variable (or lack thereof) would continue to be inherited from the Go process, as it is today.
    • If the Env field is non-nil, its PWD variable (or lack thereof) would continue to be used, as it is today.
  2. A new method, (*Cmd).Environ, analogous to os.Environ(), should return a copy of the environment that would be used by the command in its current configuration: either a copy of the current value of the Env field (if non-nil), or os.Environ() augmented with the PWD and/or SYSTEMROOT variables that would otherwise be used.

Part (1) alone would set a more appropriate PWD for existing commands that set Dir and do not explicitly modify the environment — that is, commands for which the author almost certainly hasn't considered the PWD interaction at all.

Part (2) would make it easier to set up a command that runs in a particular directory with a small number of overridden environment variables — the common case when environment variables other than PWD are changed at all.

Compatibility

This proposal would set an updated PWD variable for some commands that today receive a stale one (or none at all) inherited from the parent Go process.

It would have no effect on commands that have an empty Dir field or a non-nil Env field.

It may change the observable behavior of commands that have a nil Env but are sensitive to the value of PWD, including commands that enumerate all variables in the environment. For commands that are sensitive to the value of the PWD variable, it should nearly always be a strict improvement: the semantics of the PWD variable are explicitly described in the POSIX standard, and this change would bring the behavior in line with that standard on POSIX-like platforms.

The only case I can conceive of in which a user explicitly wants to inherit a missing or mismatched PWD is when testing the behavior of a command-line binary in a non-POSIX-compliant environment. However, for that rare case it would be straightforward for the user to explicitly set the Env field to a slice with the desired PWD variable, including no such variable at all.

Limitations

This proposal would not fix the PWD variable for commands that explicitly set Env with an incorrect or missing PWD today (such as from an explicit call to os.Environ()). That is a deliberate choice to bias toward maintaining compatibility, but may limit the scope of improvements.

Alternatives considered

  • We could set the PWD variable more or less aggressively:
    a. We could omit the PWD from the command's environment if the Go process itself has no PWD variable, even if the platform normally uses PWD.
    b. We could set PWD (on platforms that use it) whenever Dir is non-empty and Env does not include an explicit entry for it, analogous to the conditions under which we set SYSTEMROOT on Windows today. That would fix commands that set explicit variables but accidentally omit PWD, but would remove the ability to deliberately leave PWD completely unset when using the Dir field. (At best, it would be set to the empty string.)
    c. We could set PWD (on platforms that use it) whenever Dir is non-empty and Env includes a PWD that is not an absolute path that resolves to Dir. That would enforce compliance with the POSIX standard for the variable, but may be expensive to detect, and would remove the ability to deliberately use a noncompliant PWD.

In my opinion, options (a) or (b) would be reasonable but (c) would be too invasive.

  • Instead of the proposed Environ method, we could add a SetDir method that both sets the Dir field and appends an appropriate value to the Env field, populating the Env field using os.Environ() if it was previously nil.

I worry that appending to the existing Env field might be a bit too subtle: it wouldn't be intuitively obvious to me whether SetDir appends to Env (potentially overwriting entries in aliased slices), or to Env[:len(Env):len(Env)] (producing a possibly-unexpected new allocation). That could be addressed with documentation, but I think the Environ method is easier to describe, especially by analogy to os.Environ.

@gopherbot gopherbot added this to the Proposal milestone Jan 13, 2022
@rsc
Copy link
Contributor

rsc commented Jan 19, 2022

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@bcmills
Copy link
Contributor Author

bcmills commented Jan 24, 2022

Neato! One more missed PWD, found via #50183. This one can cause golang.org/dl/gotip to report a misleading GOROOT if any component of $HOME/sdk/gotip is a symlink.
https://cs.opensource.google/go/dl/+/master:internal/version/gotip.go;l=149;drc=3c153da08ce00e975a6c64a0165ba8ab96081b22

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/380503 mentions this issue: internal/version: set GOROOT_FINAL and PWD in environments

gopherbot pushed a commit to golang/dl that referenced this issue Jan 26, 2022
… Dir

Updates golang/go#50599.

Change-Id: I5702de0d6e23fa2ae1a03b0f47464170d4b691c0
Reviewed-on: https://go-review.googlesource.com/c/dl/+/380503
Trust: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
@rsc
Copy link
Contributor

rsc commented Jan 26, 2022

This seems reasonable. It's unfortunate that sometimes we will have the wrong PWD inherited from constructs like

cmd.Env = append(os.Environ(), "GOARCH=amd64")

but that's indistinguishable from really wanting to preserve PWD. So be it.

Those will just have to update to

cmd.Env = append(cmd.Environ(), "GOARCH=amd64")

@rsc
Copy link
Contributor

rsc commented Jan 26, 2022

Does anyone object to making this change?

@rsc
Copy link
Contributor

rsc commented Feb 2, 2022

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

@rsc
Copy link
Contributor

rsc commented Feb 9, 2022

No change in consensus, so accepted. 🎉
This issue now tracks the work of implementing the proposal.
— rsc for the proposal review group

@rsc rsc changed the title proposal: os/exec: improved API and default behavior for the PWD env variable os/exec: improved API and default behavior for the PWD env variable Feb 9, 2022
@rsc rsc modified the milestones: Proposal, Backlog Feb 9, 2022
@gopherbot
Copy link
Contributor

Change https://go.dev/cl/391804 mentions this issue: go/build: set PWD for go subcommands

gopherbot pushed a commit that referenced this issue Mar 14, 2022
Since these commands already include an explicit Env field,
they will not be fixed automatically by proposal #50599.

Change-Id: Ia8157a71cf0cfe208bdc0da9aef54be3d26c795f
Reviewed-on: https://go-review.googlesource.com/c/go/+/391804
Trust: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Trust: Daniel Martí <mvdan@mvdan.cc>
@philpennock
Copy link

Should linters warn if an variable c of type os/exec.Cmd has c.Environ() called on it before c.Dir is mutated?

@bcmills
Copy link
Contributor Author

bcmills commented Apr 20, 2022

Should linters warn if an variable c of type os/exec.Cmd has c.Environ() called on it before c.Dir is mutated?

Maybe! Really the condition we want to avoid is “c.Dir is set but PWD doesn't match it”, but that's harder to detect in general.

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/401339 mentions this issue: os/exec: preserve original order of entries in dedupEnv

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/401340 mentions this issue: os/exec: set PWD implicitly if Dir is non-empty and Env is nil

gopherbot pushed a commit that referenced this issue Apr 21, 2022
Once #50599 is implemented, the entries will be observable via the
Environ method. I find it confusing for later entries in the list to
jump arbitrarily far forward based on entries for the same key that no
longer exist.

This also fixes the deduplication logic for the degenerate Windows
keys observed in #49886, which were previously deduplicated as empty
keys.

(It does not do anything about the even-more-degenerate keys observed
in #52436.)

For #50599.

Change-Id: Ia7cd2200ec34ccc4b9d18631cb513194dc420c25
Reviewed-on: https://go-review.googlesource.com/c/go/+/401339
Reviewed-by: Ian Lance Taylor <iant@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
@gopherbot
Copy link
Contributor

Change https://go.dev/cl/401535 mentions this issue: go/build: replace a call to os.Environ with (*exec.Cmd).Environ

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/401534 mentions this issue: cmd/go: replace some calls to base.AppendPWD with cmd.Environ

@bcmills bcmills modified the milestones: Backlog, Go1.19 Apr 21, 2022
@bcmills bcmills self-assigned this Apr 21, 2022
gopherbot pushed a commit that referenced this issue Apr 21, 2022
With #50599 implemented, base.AppendPWD is redundant if cmd.Env would
otherwise be nil, and calls to os.Environ followed by base.AppendPWD
can be replaced by a simpler call to cmd.Environ.

Updates #50599.

Change-Id: I94a22e2a4cc8e83c815ac41702ea0b1ee5034ecc
Reviewed-on: https://go-review.googlesource.com/c/go/+/401534
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
gopherbot pushed a commit that referenced this issue Apr 21, 2022
This is a code simplification using the new API added in #50599.

Change-Id: Icb9628bcd0daa3dc2d653e9654b38099730137d1
Reviewed-on: https://go-review.googlesource.com/c/go/+/401535
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
@gopherbot
Copy link
Contributor

Change https://go.dev/cl/401934 mentions this issue: os/exec: use a TestMain to avoid hijacking stdout for helper commands

gopherbot pushed a commit that referenced this issue Apr 26, 2022
The previous implementation of helperCommand relied on running a
well-known Test function which implemented all known commands.

That not only added Skip noise in the test's output, but also (and
more importantly) meant that the commands could not write directly to
stdout in the usual way, since the testing package hijacks os.Stdout
for its own use.

The new implementation addresses the above issues, and also ensures
that all registered commands are actually used, reducing the risk of
an unused command sticking around after refactoring.

It also sets the subprocess environment variable directly in the test
process, instead of on each individual helper command's Env field,
allowing helper commands to be used without an explicit Env.

Updates #50599.
(Also for #50436.)

Change-Id: I189c7bed9a07cfe47a084b657b88575b1ee370b9
Reviewed-on: https://go-review.googlesource.com/c/go/+/401934
Run-TryBot: Bryan Mills <bcmills@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
@rsc rsc unassigned bcmills Jun 22, 2022
@gopherbot
Copy link
Contributor

Change https://go.dev/cl/484295 mentions this issue: cmd/internal/moddeps: preserve PWD more carefully in commands

gopherbot pushed a commit that referenced this issue Apr 13, 2023
On macOS, TMPDIR is typically a symlink, and the GOROOT for the
buildlet is in TMPDIR as well. PWD must be preserved in order for
os.Getwd (and functions based on it) to report paths that remain
relative to GOROOT, and paths relative to GOROOT are necessary in
order for filepath.Rel to report subdirectories as subdirectories
(rather than paths with long "../../…" prefixes).

Fortunately, the (*Cmd).Environ method added for #50599 makes
preserving PWD somewhat easier.

This fixes 'go test cmd/internal/moddeps' on the new
darwin-amd64-longtest builder.

For #35678.

Change-Id: Ibaa458bc9a94b44ba455519bb8da445af07fe0d2
Reviewed-on: https://go-review.googlesource.com/c/go/+/484295
Auto-Submit: Bryan Mills <bcmills@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Michael Pratt <mpratt@google.com>
@golang golang locked and limited conversation to collaborators Apr 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants