Skip to content
Merged
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
2 changes: 1 addition & 1 deletion crates/terraphim_lsp/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/terraphim_lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ resolver = "2"
members = [".", "../terraphim_lsp_core"]

[workspace.package]
version = "1.22.0-rc.4"
version = "1.22.0-rc.5"
edition = "2024"
rust-version = "1.91"

Expand Down
48 changes: 33 additions & 15 deletions crates/terraphim_lsp/tests/stdio_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,37 @@ fn frame(message: &serde_json::Value) -> Vec<u8> {
format!("Content-Length: {}\r\n\r\n{body}", body.len()).into_bytes()
}

/// Observe an actual response before advancing the client handshake. Answer
/// refresh requests while preserving every raw server message for assertions.
/// Preserve every frame and acknowledge server requests as a real client does.
fn receive_message(
receiver: &mpsc::Receiver<String>,
stdin: &mut std::process::ChildStdin,
seen: &mut Vec<String>,
) -> serde_json::Value {
let message = receiver
.recv_timeout(Duration::from_secs(30))
.unwrap_or_else(|_| panic!("no next server frame; received {seen:?}"));
let value: serde_json::Value = serde_json::from_str(&message).unwrap();
seen.push(message);
if value["method"] == "workspace/diagnostic/refresh" {
stdin
.write_all(&frame(&serde_json::json!({
"jsonrpc": "2.0", "id": value["id"], "result": null,
})))
.unwrap();
stdin.flush().unwrap();
}
value
}

/// Observe an actual response before advancing the client handshake.
fn receive_response(
receiver: &mpsc::Receiver<String>,
stdin: &mut std::process::ChildStdin,
seen: &mut Vec<String>,
id: i64,
) -> serde_json::Value {
loop {
let message = receiver
.recv_timeout(Duration::from_secs(30))
.unwrap_or_else(|_| panic!("no response {id}; received {seen:?}"));
let value: serde_json::Value = serde_json::from_str(&message).unwrap();
seen.push(message);
if value["method"] == "workspace/diagnostic/refresh" {
stdin
.write_all(&frame(&serde_json::json!({
"jsonrpc": "2.0", "id": value["id"], "result": null,
})))
.unwrap();
stdin.flush().unwrap();
}
let value = receive_message(receiver, stdin, seen);
if value["id"] == id && value.get("method").is_none() {
assert!(value.get("error").is_none(), "response {id}: {value}");
return value;
Expand All @@ -48,6 +57,7 @@ fn receive_response(
/// order. Complete initialize before sending normal traffic, and shutdown
/// before exit, just as a conforming LSP client does.
fn session(workspace: serde_json::Value) -> Vec<String> {
let expects_refresh = workspace["diagnostics"]["refreshSupport"] == true;
let mut child = Command::new(env!("CARGO_BIN_EXE_terraphim-lsp"))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
Expand Down Expand Up @@ -113,6 +123,14 @@ fn session(workspace: serde_json::Value) -> Vec<String> {

let trim = receive_response(&receiver, &mut stdin, &mut seen, 2);
assert!(trim["result"]["candidates"].is_u64(), "{trim}");
// The refresh is deliberately spawned: the command response is not a
// barrier for that independent server request. Keep the client alive and
// acknowledge it before shutdown, whichever message arrived first.
if expects_refresh {
while refreshes(&seen) == 0 {
receive_message(&receiver, &mut stdin, &mut seen);
}
}
stdin
.write_all(&frame(
&serde_json::json!({"jsonrpc": "2.0", "id": 3, "method": "shutdown"}),
Expand Down
25 changes: 22 additions & 3 deletions docs/plans/public-lsp-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ change. The original `alternative.add` result remains unchanged.

- `terraphim_lsp_core` is version `1.23.1`, with public dependencies on types
`1.22.3`, automata `2.1.0`, alternatives `0.1.1` and Lab `0.1.1`.
- `terraphim_lsp` is the `1.22.0-rc.4` candidate and depends on core `1.23.1`.
- `terraphim_lsp` is the `1.22.0-rc.5` candidate and depends on core `1.23.1`.
- Both packages restrict publication to crates.io and include the Apache-2.0
licence notice and their own test fixtures. No private Git history, registry
credentials or operational files are part of the snapshot.
Expand Down Expand Up @@ -63,8 +63,8 @@ for public registry sources, builds the package, and tests the extracted bytes
outside the source checkout. A second job reproduces the qualified archive
before requesting publication credentials.

Publish `terraphim_lsp_core-v1.23.1` first, then
`terraphim_lsp-v1.22.0-rc.4`. Existing crates use the `crates-io` environment and
Core `terraphim_lsp_core-v1.23.1` is already published. The next server tag is
`terraphim_lsp-v1.22.0-rc.5`; the published RC4 crate and source tag remain immutable. Existing crates use the `crates-io` environment and
crates.io trusted publishing. A first-ever crate name uses a deliberate manual
run on its exact tag with `auth_mode=bootstrap` and the `crates-io-bootstrap`
environment. This temporary path reads the existing `CARGO_REGISTRY_TOKEN`
Expand Down Expand Up @@ -107,3 +107,22 @@ The earlier private-source producer remains manually dispatchable for its
separate provenance work, but it does not publish releases. The public binary
publisher accepts only the reviewed crate workflow call and no longer consumes
private producer artifacts. The RC4 release uses public package provenance.

## Cross build directory correction

RC4 crate publication succeeded, but its musl binary lanes failed before
compilation. The pinned Cross wrapper maps `CARGO_TARGET_DIR` to `/target` while
forwarding `CARGO_BUILD_BUILD_DIR` unchanged. An absolute host build directory
therefore pointed outside the writable container mount. For Cross build commands
only, the helper now omits that redundant override so Cargo defaults to the
mapped target directory. Native command environments and filesystem permissions
remain unchanged.

The original failure and correction were reproduced with actual non-root Cargo
1.96 containers. Both full musl builds of the already-published RC4 crate then
passed using pinned Cross revision `29d00c7803f221f1b3f35e561b03792368fb8339` and
the corrected helper. The x86-64 binary also passed actual version, save/reopen
and synonym-export protocol checks; ARM64 was architecture-checked without a
native runtime claim. These diagnostic outputs are not published release assets.
RC5 changes the server release identity and build helper; core 1.23.1 remains
unchanged. The normal release workflow must qualify and publish the new source.
2 changes: 1 addition & 1 deletion release/lsp-crates.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
{
"name": "terraphim_lsp",
"version": "1.22.0-rc.4",
"version": "1.22.0-rc.5",
"path": "crates/terraphim_lsp/Cargo.toml"
}
]
Expand Down
13 changes: 9 additions & 4 deletions scripts/lsp_public_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,26 @@ def prepare(root, lane, expected, item):
return source_receipt(lane, expected)


def environment(lane):
def environment(lane, *, for_cross=False):
env = {k: v for k, v in os.environ.items() if not k.startswith(('CARGO_', 'RUSTC_', 'RUSTFLAGS', 'GH_', 'GITHUB_TOKEN', 'GITEA_', 'OP_'))}
for key in ('RUSTC', 'RUSTDOC', 'RUSTUP_TOOLCHAIN'):
env.pop(key, None)
env.update({'CARGO_HOME': str(lane / 'cargo'), 'CARGO_TARGET_DIR': str(lane / 'target'),
'CARGO_BUILD_BUILD_DIR': str(lane / 'target'), 'CARGO_TERM_COLOR': 'never',
'RUSTC_WRAPPER': '', 'RUSTC_WORKSPACE_WRAPPER': ''})
if for_cross:
# Cross maps CARGO_TARGET_DIR to /target inside its container, but the
# pinned wrapper forwards CARGO_BUILD_BUILD_DIR unchanged. Let Cargo
# default its build directory to that mapped target, never a host path.
env.pop('CARGO_BUILD_BUILD_DIR', None)
return env


def command(argv, cwd, lane, name):
def command(argv, cwd, lane, name, *, for_cross=False):
logs = lane / 'logs'; logs.mkdir(exist_ok=True)
path = logs / (name + '.log')
with path.open('xb') as output:
result = subprocess.run(argv, cwd=cwd, env=environment(lane), stdout=output,
result = subprocess.run(argv, cwd=cwd, env=environment(lane, for_cross=for_cross), stdout=output,
stderr=subprocess.STDOUT, timeout=3600, check=False)
record = {'argv': argv, 'exitCode': result.returncode, 'log': path.name, 'sha256': publisher.digest(path)}
require(result.returncode == 0, 'Build command failed: ' + name)
Expand Down Expand Up @@ -374,7 +379,7 @@ def build(lane, expected, target, cross_image=None):
invocation = ['rustup', 'run', expected['toolchain'], 'cross']
else:
invocation = cargo
commands.append(command(invocation + ['build', '--locked', '--offline', '--release', '--target', target, '--bin', 'terraphim-lsp'], source, lane, 'build-' + target))
commands.append(command(invocation + ['build', '--locked', '--offline', '--release', '--target', target, '--bin', 'terraphim-lsp'], source, lane, 'build-' + target, for_cross=cross_image is not None))
return stage_binary(lane / 'target' / target / 'release/terraphim-lsp', target, lane, expected, commands,
{'crossImage': cross_image, 'crossImageSelection': 'Runtime digest resolved from upstream 0.2.5 tag; the recorded digest is used for this build.'} if cross_image else {})

Expand Down
30 changes: 30 additions & 0 deletions tests/test_lsp_public_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,36 @@ def test_real_subprocess_log_is_saved_and_hash_bound(self):
b.command([sys.executable,'-c','raise SystemExit(4)'],lane,lane,'real-failure')
self.assertTrue((lane/'logs/real-failure.log').is_file())

def test_cross_environment_omits_host_build_dir_without_changing_native(self):
lane = self.root / 'lane'
previous = os.environ.get('CARGO_BUILD_BUILD_DIR')
try:
os.environ['CARGO_BUILD_BUILD_DIR'] = '/unrelated/inherited/build'
native = b.environment(lane)
cross = b.environment(lane, for_cross=True)
finally:
if previous is None:
os.environ.pop('CARGO_BUILD_BUILD_DIR', None)
else:
os.environ['CARGO_BUILD_BUILD_DIR'] = previous
self.assertEqual(native['CARGO_BUILD_BUILD_DIR'], str(lane / 'target'))
self.assertNotIn('CARGO_BUILD_BUILD_DIR', cross)
self.assertEqual(cross['CARGO_TARGET_DIR'], str(lane / 'target'))
self.assertEqual({k: v for k, v in native.items() if k != 'CARGO_BUILD_BUILD_DIR'}, cross)

def test_actual_command_child_receives_cross_specific_environment(self):
lane = self.root / 'child-env'; lane.mkdir()
command = [sys.executable, '-c',
'import json,os; print(json.dumps({k:os.environ.get(k) for k in '
'["CARGO_BUILD_BUILD_DIR","CARGO_TARGET_DIR"]}))']
b.command(command, lane, lane, 'native-env')
b.command(command, lane, lane, 'cross-env', for_cross=True)
native = json.loads((lane / 'logs/native-env.log').read_text())
cross = json.loads((lane / 'logs/cross-env.log').read_text())
self.assertEqual(native['CARGO_BUILD_BUILD_DIR'], str(lane / 'target'))
self.assertIsNone(cross['CARGO_BUILD_BUILD_DIR'])
self.assertEqual(cross['CARGO_TARGET_DIR'], native['CARGO_TARGET_DIR'])

def test_architecture_refuses_unknown_and_mismatched_format(self):
for target in (*b.TARGETS,'universal-apple-darwin'):
path=self.root/target;path.write_bytes(self.executable(target));self.assertTrue(b.architecture(path))
Expand Down
Loading