Skip to content

Commit 85a1b1b

Browse files
committed
feat(diagnostics): add process environment overrides
1 parent 5ca1c7f commit 85a1b1b

8 files changed

Lines changed: 107 additions & 16 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
## Non-negotiable rules
1010

1111
1. **Zero errors, zero warnings** before any commit. `cmake --build build --parallel 4` must be clean.
12-
2. **No test-count regression.** `scripts/run_component_tests.sh build` must show no failures. The verified baseline is 12,675 tests across 36 component executables and one integration executable; this floor should be raised as new tests are added and lowered only with an explicit, documented reason.
12+
2. **No test-count regression.** `scripts/run_component_tests.sh build` must show no failures. The verified baseline is 12,678 tests across 36 component executables and one integration executable; this floor should be raised as new tests are added and lowered only with an explicit, documented reason.
1313
3. **Push only to `feature/work`.** Never push to `develop` or `master`, and never create tags, without explicit per-action user approval.
1414
4. **SPDX header on every project source/header**`// SPDX-License-Identifier: MIT` + copyright + .NET attribution. Vendored sources retain their upstream headers; Markdown uses an HTML SPDX comment where one is present.
1515
5. **Property naming:** always `getXxxProperty()` / `setXxxProperty()`. Exception: indexers

NEXT.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
# NEXT.md
55

66
*Last verified: 2026-07-25. Branch: `feature/work`. The P0 component-boundary
7-
repair, three P1 parity repairs, P1 portability revalidation, and twenty bounded
7+
repair, three P1 parity repairs, P1 portability revalidation, and twenty-one bounded
88
P2 API slices are complete: 41 physical modules, 90 production dependency
9-
edges, and 12,675 tests across 37 executables.*
9+
edges, and 12,678 tests across 37 executables.*
1010

1111
This is the cold-start handoff for the next working session. Keep it focused
1212
on verified facts, remaining bounded work, and commands needed to resume.
@@ -28,7 +28,7 @@ Historical session detail belongs in git history and `plan.sqlite3`.
2828
- The ten-job selective consumer matrix, including a direct
2929
`Collections.Blocking` consumer, is green. Text.Json retains its target
3030
absence and negative include-leakage assertions.
31-
- The full native baseline is a warning-free build with 12,675 passing tests
31+
- The full native baseline is a warning-free build with 12,678 passing tests
3232
across 36 component executables and one integration executable.
3333
- `TaskT<TResult>::ContinueWith` now supports both action and result-producing
3434
callbacks. It runs inline on completion; `NotOn*` and `OnlyOn*` filter the
@@ -80,6 +80,9 @@ Historical session detail belongs in git history and `plan.sqlite3`.
8080
- `Trace::WriteIf` and `Trace::WriteLineIf` now conditionally preserve the
8181
existing stderr write/newline behavior; category and listener surfaces stay
8282
intentionally deferred.
83+
- `ProcessStartInfo` now supplies explicit child-only environment overrides;
84+
unspecified values inherit, empty values remain empty, and invalid variable
85+
names are rejected before forking.
8386
- MinGW-w64 GCC 14-win32/CMake 3.31.6 and Emscripten 5.0.7/CMake 3.31.6 both
8487
compile the post-modular `All` graph and selective `Text.Json` libraries.
8588
This is compile-only evidence: cross tests were deliberately disabled.
@@ -88,7 +91,7 @@ Historical session detail belongs in git history and `plan.sqlite3`.
8891
ASan/LSan ownership scenarios, including 100 continuation teardowns, pass.
8992

9093
The local `plan.sqlite3` snapshot contains 16,201 classified `task` rows and
91-
1,762 completed tickets. Ticket #1737 records the completed P0 split, tickets
94+
1,763 completed tickets. Ticket #1737 records the completed P0 split, tickets
9295
#1738/#1739 the MemoryStream and generic-continuation repairs, ticket #1740 the
9396
XML whitespace repair, #1741 the completed cross-build revalidation and
9497
`WebProxy` portability fix, #1742 focused sanitizer evidence, and #1743 the
@@ -103,7 +106,8 @@ queries, #1755 for its comparer-aware binary search, #1756 for its default
103106
range sort, #1757 for `BigInteger` bitwise operators, #1758 for signed
104107
`BigInteger` shifts, #1759 for byte-vector conversion, and #1760 for the
105108
`ImmutableList<T>` Builder core, #1761 for RFC 2152 UTF-7, and #1762 for
106-
conditional Trace writes; it is git-ignored and is not part of a fresh clone.
109+
conditional Trace writes, and #1763 for ProcessStartInfo environment overrides;
110+
it is git-ignored and is not part of a fresh clone.
107111

108112
## P0 completion: restore Collections isolation
109113

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ collector, or the complete .NET platform.
77

88
The repository currently builds as 41 independently selectable CMake
99
components. The verified Linux baseline on 2026-07-25 is a warning-free build
10-
with **12,675 passing tests across 37 test executables**.
10+
with **12,678 passing tests across 37 test executables**.
1111

1212
## What is included
1313

@@ -181,7 +181,7 @@ Other platform evidence is narrower:
181181

182182
| Platform/toolchain | Verified scope |
183183
|---|---|
184-
| Linux/GCC | Current full component build and all 12,675 tests. |
184+
| Linux/GCC | Current full component build and all 12,678 tests. |
185185
| Windows/MinGW | MinGW-w64 GCC 14-win32/CMake 3.31.6 compiled the post-component `All` and selective `Text.Json` library graphs under ticket #1741. GoogleTest was not cross-built and repository CI remains Ubuntu-only. |
186186
| Emscripten | Emscripten 5.0.7/CMake 3.31.6 compiled the post-component `All` and selective `Text.Json` library graphs under ticket #1741. Tests were not cross-built or run, and some runtime APIs deliberately throw `PlatformNotSupportedException`. |
187187
| macOS/Apple Clang | Real downstream Xcode 15.4 builds drove portability fixes on 2026-07-20; this repository has no macOS job or recorded full standalone test baseline. |

docs/CMakeComponents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ are named `SharpRuntimeTests_<Component>`; genuinely cross-module scenarios
131131
are in `SharpRuntimeIntegrationTests`. CTest also discovers every individual
132132
GoogleTest case.
133133

134-
The verified 2026-07-25 `All` baseline contains 12,675 tests across 36
134+
The verified 2026-07-25 `All` baseline contains 12,678 tests across 36
135135
component executables and one integration executable.
136136

137137
## Boundary validation and CI

modules/diagnostics/include/System/Diagnostics/ProcessStartInfo.hpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#pragma once
55

6+
#include <map>
67
#include <string>
78
#include <vector>
89

@@ -23,16 +24,17 @@ namespace System::Diagnostics {
2324
* Windows; on POSIX, argv is always a plain string array with no re-quoting step at all) --
2425
* use @ref getArgumentListProperty directly for anything containing spaces or quotes.
2526
*
26-
* @note Status: Partial. UseShellExecute, redirected standard input, environment-variable
27-
* overrides, and window-style/verb properties (all Windows-shell-specific or otherwise out of
28-
* scope for a POSIX-only game-runtime launcher) are not implemented -- see
27+
* @note Status: Partial. UseShellExecute, redirected standard input, environment enumeration
28+
* and removal semantics, and window-style/verb properties (all Windows-shell-specific or
29+
* otherwise out of scope for a POSIX-only game-runtime launcher) are not implemented -- see
2930
* System::Diagnostics::Process's class doc-comment for the full list of deliberately deferred
3031
* surface.
3132
*/
3233
class ProcessStartInfo {
3334
std::string fileName_;
3435
std::string arguments_;
3536
std::vector<std::string> argumentList_;
37+
std::map<std::string, std::string> environmentVariables_;
3638
std::string workingDirectory_;
3739
bool redirectStandardOutput_ = false;
3840
bool redirectStandardError_ = false;
@@ -63,6 +65,20 @@ namespace System::Diagnostics {
6365
[[nodiscard]] std::vector<std::string>& getArgumentListProperty() { return argumentList_; }
6466
[[nodiscard]] const std::vector<std::string>& getArgumentListProperty() const { return argumentList_; }
6567

68+
/**
69+
* @brief Gets explicitly supplied child environment-variable overrides.
70+
*
71+
* Entries are applied only to the launched child; unspecified variables are inherited
72+
* from the parent. Variable names must be nonempty and must not contain '=' when Start()
73+
* is called. An empty value is passed to the child as an empty variable value.
74+
*/
75+
[[nodiscard]] std::map<std::string, std::string>& getEnvironmentVariablesProperty() {
76+
return environmentVariables_;
77+
}
78+
[[nodiscard]] const std::map<std::string, std::string>& getEnvironmentVariablesProperty() const {
79+
return environmentVariables_;
80+
}
81+
6682
/** @brief Gets or sets the working directory for the process. Empty means inherit the caller's current directory. */
6783
[[nodiscard]] const std::string& getWorkingDirectoryProperty() const { return workingDirectory_; }
6884
void setWorkingDirectoryProperty(const std::string& value) { workingDirectory_ = value; }

modules/diagnostics/src/System/Diagnostics/Process.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright (c) Robert Vokac and contributors
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#include "System/Diagnostics/Process.hpp"
5+
#include "System/ArgumentException.hpp"
56
#include "System/InvalidOperationException.hpp"
67
#include "System/PlatformNotSupportedException.hpp"
78

@@ -134,6 +135,13 @@ bool Process::Start() {
134135
const ProcessStartInfo& si = impl_->startInfo;
135136
if (si.getFileNameProperty().empty())
136137
throw System::InvalidOperationException("Cannot start process because a file name has not been provided.");
138+
for (const auto& [name, value] : si.getEnvironmentVariablesProperty()) {
139+
(void)value;
140+
if (name.empty() || name.find('=') != std::string::npos) {
141+
throw System::ArgumentException(
142+
"Environment variable names must be nonempty and cannot contain '='.", "name");
143+
}
144+
}
137145

138146
// Build argv: argv[0] is conventionally the program name, followed by ArgumentList (the
139147
// unambiguous path -- no shell/OS quoting), then a naive whitespace split of Arguments for
@@ -181,6 +189,9 @@ bool Process::Start() {
181189
if (!si.getWorkingDirectoryProperty().empty()) {
182190
if (::chdir(si.getWorkingDirectoryProperty().c_str()) != 0) ::_exit(127);
183191
}
192+
for (const auto& [name, value] : si.getEnvironmentVariablesProperty()) {
193+
if (::setenv(name.c_str(), value.c_str(), 1) != 0) ::_exit(127);
194+
}
184195
::execvp(argv[0], argv.data());
185196
::_exit(127); // execvp only returns on failure
186197
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) Robert Vokac and contributors
3+
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
4+
#include <gtest/gtest.h>
5+
6+
#include <cstdlib>
7+
#include <string>
8+
9+
#include "System/ArgumentException.hpp"
10+
#include "System/Diagnostics/Process.hpp"
11+
12+
using System::Diagnostics::Process;
13+
using System::Diagnostics::ProcessStartInfo;
14+
15+
namespace {
16+
constexpr const char* EnvironmentKey = "SHARP_RUNTIME_PROCESS_ENV_TEST_1763";
17+
}
18+
19+
TEST(ProcessEnvironmentOverrideTest, ChildReceivesOverrideWithoutChangingParentEnvironment) {
20+
const char* before = std::getenv(EnvironmentKey);
21+
const std::string beforeValue = before == nullptr ? "" : before;
22+
const bool hadBeforeValue = before != nullptr;
23+
24+
ProcessStartInfo startInfo("/bin/sh");
25+
startInfo.getArgumentListProperty().push_back("-c");
26+
startInfo.getArgumentListProperty().push_back("printf '%s' \"$SHARP_RUNTIME_PROCESS_ENV_TEST_1763\"");
27+
startInfo.getEnvironmentVariablesProperty()[EnvironmentKey] = "child-value";
28+
startInfo.setRedirectStandardOutputProperty(true);
29+
Process process = Process::Start(startInfo);
30+
process.WaitForExit();
31+
32+
EXPECT_EQ(process.getExitCodeProperty(), 0);
33+
EXPECT_EQ(process.getStandardOutputTextProperty(), "child-value");
34+
const char* after = std::getenv(EnvironmentKey);
35+
EXPECT_EQ(after != nullptr, hadBeforeValue);
36+
if (after != nullptr) {
37+
EXPECT_EQ(std::string(after), beforeValue);
38+
}
39+
}
40+
41+
TEST(ProcessEnvironmentOverrideTest, EmptyValueIsPassedLiterallyToChild) {
42+
ProcessStartInfo startInfo("/bin/sh");
43+
startInfo.getArgumentListProperty().push_back("-c");
44+
startInfo.getArgumentListProperty().push_back("test -z \"$SHARP_RUNTIME_PROCESS_ENV_TEST_1763\"");
45+
startInfo.getEnvironmentVariablesProperty()[EnvironmentKey] = "";
46+
Process process = Process::Start(startInfo);
47+
process.WaitForExit();
48+
49+
EXPECT_EQ(process.getExitCodeProperty(), 0);
50+
}
51+
52+
TEST(ProcessEnvironmentOverrideTest, InvalidVariableNamesAreRejectedBeforeForking) {
53+
ProcessStartInfo startInfo("/bin/true");
54+
startInfo.getEnvironmentVariablesProperty()["INVALID=NAME"] = "value";
55+
56+
EXPECT_THROW(Process::Start(startInfo), System::ArgumentException);
57+
}

plan.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Sharp Runtime plan
22

33
*Last verified: 2026-07-25 — 41 physical components, 90 direct production
4-
dependency edges, a clean native build, 12,675 passing tests across 37
4+
dependency edges, a clean native build, 12,678 passing tests across 37
55
executables, and a green ten-job selective matrix.*
66

77
Sharp Runtime is in a consumer-driven expansion phase. The original type
@@ -32,7 +32,7 @@ was never created. Neither file should be linked as current documentation.
3232
### Code and validation
3333

3434
- Native Linux/GCC build: zero errors and zero warnings.
35-
- Tests: 12,675 passing across 36 component binaries plus one integration
35+
- Tests: 12,678 passing across 36 component binaries plus one integration
3636
binary.
3737
- Component graph: 41 physical modules and 90 direct production edges.
3838
- Boundary validator: no cycles, duplicate public include paths, orphan
@@ -52,7 +52,7 @@ The 2026-07-25 local snapshot contains:
5252
| Table | State |
5353
|---|---|
5454
| `task` | 16,201 rows: 1,082 `ported`, 140 `ignore`, 14,979 legacy `ignored`; no unclassified or `tobedecided` rows |
55-
| `ticket` | 1,762 rows, all `done`; no `todo`, `doing`, `blocked`, or `needs_user` rows |
55+
| `ticket` | 1,763 rows, all `done`; no `todo`, `doing`, `blocked`, or `needs_user` rows |
5656

5757
Because `plan.sqlite3` is git-ignored, these counts describe the maintainer
5858
snapshot, not data shipped in a fresh clone.
@@ -151,6 +151,9 @@ assertion without an explicit architecture decision.
151151
- Added conditional `Trace::WriteIf` and `Trace::WriteLineIf` under ticket
152152
#1762. They suppress output when false and retain the existing stderr
153153
write/newline behavior when true.
154+
- Added explicit ProcessStartInfo child environment overrides under ticket
155+
#1763. They are validated before fork, passed only to the child, and retain
156+
inherited values not explicitly overridden.
154157
- Added consumer-driven coverage across core, collections, IO, networking,
155158
threading/tasks, text/JSON, XML, numerics, globalization, and cryptographic
156159
hashing/random APIs.
@@ -211,7 +214,7 @@ The first consumer-driven ports after modularization added:
211214
- XML schema exception types.
212215

213216
The verified test baseline grew from 12,494 at the modularization checkpoint
214-
to 12,675.
217+
to 12,678.
215218

216219
## Candidate roadmap
217220

0 commit comments

Comments
 (0)