|
| 1 | +// Package subprocenv provides environment sanitisation for spawning external |
| 2 | +// processes from a PyInstaller-frozen binary. |
| 3 | +// |
| 4 | +// When APM ships as a PyInstaller --onedir binary the bootloader prepends |
| 5 | +// the bundle's _internal directory to LD_LIBRARY_PATH (Linux) and the |
| 6 | +// DYLD_* variables (macOS) so that the main Python process can find its own |
| 7 | +// shared libraries. Child processes inherit that environment by default, |
| 8 | +// which causes system binaries (git, curl, the install script) to resolve |
| 9 | +// their dependencies against the bundled libraries. This package centralises |
| 10 | +// the restoration logic that mirrors the Python subprocess_env module. |
| 11 | +package subprocenv |
| 12 | + |
| 13 | +import ( |
| 14 | + "os" |
| 15 | + "runtime" |
| 16 | + "strings" |
| 17 | +) |
| 18 | + |
| 19 | +// pyinstallerManagedVars are the library-path variables that PyInstaller's |
| 20 | +// bootloader rewrites at launch. Each has a sibling <NAME>_ORIG holding the |
| 21 | +// pre-launch value that must be restored before handing the environment to a |
| 22 | +// child process. |
| 23 | +var pyinstallerManagedVars = []string{ |
| 24 | + "LD_LIBRARY_PATH", // Linux and most Unixes |
| 25 | + "DYLD_LIBRARY_PATH", // macOS dynamic library search path |
| 26 | + "DYLD_FRAMEWORK_PATH", // macOS framework search path |
| 27 | +} |
| 28 | + |
| 29 | +// isFrozen returns true when the process was started by PyInstaller. This is |
| 30 | +// detected by checking for the _MEIPASS environment variable that PyInstaller |
| 31 | +// always sets in a frozen binary. |
| 32 | +func isFrozen() bool { |
| 33 | + _, ok := os.LookupEnv("_MEIPASS") |
| 34 | + return ok |
| 35 | +} |
| 36 | + |
| 37 | +// ExternalProcessEnv returns an environment map safe for spawning external |
| 38 | +// system binaries. |
| 39 | +// |
| 40 | +// When not running as a PyInstaller-frozen binary the current os.Environ() is |
| 41 | +// returned as a fresh map with no modifications. |
| 42 | +// |
| 43 | +// When frozen, every library-path variable in pyinstallerManagedVars is |
| 44 | +// restored from its <NAME>_ORIG sibling. If no _ORIG sibling exists the |
| 45 | +// variable is removed entirely so the child does not inherit the bundle's |
| 46 | +// _internal path. The _ORIG keys themselves are stripped. |
| 47 | +// |
| 48 | +// If base is non-nil it is used as the source mapping instead of os.Environ(). |
| 49 | +func ExternalProcessEnv(base map[string]string) map[string]string { |
| 50 | + env := envToMap(base) |
| 51 | + |
| 52 | + if !isFrozen() { |
| 53 | + return env |
| 54 | + } |
| 55 | + |
| 56 | + for _, key := range pyinstallerManagedVars { |
| 57 | + origKey := key + "_ORIG" |
| 58 | + if origVal, ok := env[origKey]; ok { |
| 59 | + env[key] = origVal |
| 60 | + delete(env, origKey) |
| 61 | + } else { |
| 62 | + delete(env, key) |
| 63 | + } |
| 64 | + } |
| 65 | + return env |
| 66 | +} |
| 67 | + |
| 68 | +// envToMap converts a []string slice (KEY=VALUE pairs) or an existing map into |
| 69 | +// a fresh map[string]string copy. When base is nil os.Environ() is used. |
| 70 | +func envToMap(base map[string]string) map[string]string { |
| 71 | + if base != nil { |
| 72 | + out := make(map[string]string, len(base)) |
| 73 | + for k, v := range base { |
| 74 | + out[k] = v |
| 75 | + } |
| 76 | + return out |
| 77 | + } |
| 78 | + pairs := os.Environ() |
| 79 | + out := make(map[string]string, len(pairs)) |
| 80 | + for _, pair := range pairs { |
| 81 | + idx := strings.IndexByte(pair, '=') |
| 82 | + if idx < 0 { |
| 83 | + out[pair] = "" |
| 84 | + continue |
| 85 | + } |
| 86 | + out[pair[:idx]] = pair[idx+1:] |
| 87 | + } |
| 88 | + return out |
| 89 | +} |
| 90 | + |
| 91 | +// MapToSlice converts a map[string]string into a []string of KEY=VALUE pairs |
| 92 | +// suitable for exec.Cmd.Env. |
| 93 | +func MapToSlice(env map[string]string) []string { |
| 94 | + out := make([]string, 0, len(env)) |
| 95 | + for k, v := range env { |
| 96 | + out = append(out, k+"="+v) |
| 97 | + } |
| 98 | + return out |
| 99 | +} |
| 100 | + |
| 101 | +// IsWindows reports whether the current OS is Windows. |
| 102 | +func IsWindows() bool { |
| 103 | + return runtime.GOOS == "windows" |
| 104 | +} |
0 commit comments