Skip to content

fix: add macOS Apple Silicon support for VHDL and Verilog language servers#845

Open
alexmodrono wants to merge 11 commits intoTerosTechnology:devfrom
alexmodrono:fix/macos-language-server-support
Open

fix: add macOS Apple Silicon support for VHDL and Verilog language servers#845
alexmodrono wants to merge 11 commits intoTerosTechnology:devfrom
alexmodrono:fix/macos-language-server-support

Conversation

@alexmodrono
Copy link

@alexmodrono alexmodrono commented Feb 23, 2026

Summary

This PR fixes several bugs that prevented TerosHDL from working correctly on macOS (particularly Apple Silicon), and fixes a formatter regression that affected all platforms.


1. Bundle vhdl-ls v0.84.0 for macOS Apple Silicon + system binary fallback

The extension only shipped Linux/Windows binaries. macOS ARM64 users had no bundled server and no fallback path. Two changes:

  • Bundle the vhdl_ls-aarch64-apple-darwin binary (v0.84.0) under server/vhdl_ls/
  • If the bundled binary fails (wrong arch, missing, etc.), automatically try /opt/homebrew/bin/vhdl_ls, /usr/local/bin/vhdl_ls, and vhdl_ls in PATH
  • Same fallback pattern applied to Verible

2. Fix --libraries argument (directory vs. file path)

vhdl_ls 0.84.0 expects the --libraries flag to point to the directory that contains vhdl_ls.toml, not to the file itself. The previous code passed the full file path, causing:

Not a directory (os error 20)

on every VHDL file open, which prevented the bundled IEEE/STD library from loading and crashed the server.


3. Fix context.asAbsolutePath() corrupting system binary paths

context.asAbsolutePath(str) calls path.join(extensionRoot, str). When str is already an absolute path like /opt/homebrew/bin/vhdl_ls, path.join prepends the extension directory, producing a broken path like /ext/path/opt/homebrew/bin/vhdl_ls.

Fix: resolve the bundled binary's absolute path once at startup and store it directly, so getServerOptionsEmbedded never needs to call asAbsolutePath.


4. Fix work library name in generated TOML

vhdl_ls 0.84.0 rejects work as an explicit library name in project config TOML (it is a reserved identifier). When files in a project have logical_name = "work", get_toml() was emitting work.files = [...], causing:

Error loading vhdl_ls.toml: The 'work' library is not a valid library.

Fix: remap library name workteroshdlDefault in project_manager.ts.


5. Fix duplicate verible.restart command registration

The extension was registering the teroshdl.verible.restart command twice, which caused an activation error when opening Verilog/SV files. Removed the redundant empty registration.


6. Initialise VHDL_LS_CONFIG temp file with valid content

The temp file used for VHDL_LS_CONFIG was created with an empty string, causing:

missing field `libraries`

on startup. The server would then fall through to auto-discover any vhdl_ls.toml in the workspace root, bypassing the project config entirely. Fix: initialise the file with [libraries]\n so vhdl_ls always has a valid minimal config.


7. Inherit full process environment for the language server

ServerOptions.options.env in vscode-languageclient replaces the entire child process environment (Node.js child_process.spawn behaviour). The previous code only passed { VHDL_LS_CONFIG: "..." }, stripping PATH, HOME, TMPDIR, and every other OS-level variable from the vhdl_ls process.

Fix: spread process.env first so vhdl_ls inherits the full environment, with VHDL_LS_CONFIG overriding/adding on top.


8. Fix duplicate forceRefresh causing unnecessary SIGKILL at startup

At startup, the teroshdl.vhdlls.restart command was being scheduled twice in quick succession:

  1. Tree_view_manager constructor fires GLOBAL_REFRESHrefreshToml()forceRefresh() → restart
  2. init_tree_views() then also called forceRefresh() explicitly

Each restart call sends an LSP shutdown to the running vhdl_ls. If the server is busy (indexing files), it does not respond within the timeout and VS Code sends SIGKILL, then spawns a new instance. The double call meant two SIGKILLs back-to-back on startup.

Fix: remove the redundant explicit forceRefresh call in init_tree_views; the GLOBAL_REFRESH event from the Tree_view_manager constructor already handles it.


9. Fix VSG formatter not applying edits to files with violations (all platforms)

This is a regression introduced in commit 63e925f. The formatter was changed to check exec_result.return_value === 0 for success, but the underlying local_process.ts always stores -1 in return_value for any non-zero exit — it was ignoring error.code from Node's ExecException.

VSG exits with:

  • 0 — no violations found (file was already clean)
  • 1 — violations found and fixed with --fix (the normal formatting outcome)

With return_value always being -1 for exit code 1, the success check return_value === 0 || return_value === 1 never matched exit code 1. successful was false, the edit was discarded, and the file appeared unchanged.

Only files that were already fully compliant (exit 0) ever got an edit applied, but all others did not.

Two-part fix:

  • local_process.ts: store the actual exit code from error.code instead of always using -1. This is correct for all tools.
  • vsg.ts: the existing return_value === 0 || return_value === 1 check now works as intended.

Test plan

  • Open a .vhd file on macOS Apple Silicon — VHDL LS starts without crashing
  • "VHDL LS" Output channel shows all three config lines with no errors:
    • Loaded Installation configuration file: .../vhdl_libraries/vhdl_ls.toml
    • Loaded VHDL_LS_CONFIG configuration file: ...
    • Loaded workspace root configuration file: ... (if workspace has vhdl_ls.toml)
  • Hover over a VHDL identifier — IEEE/STD type info appears
  • Go-to-definition works for VHDL entities
  • Open a .v/.sv file — no duplicate command activation error
  • Format a VHDL file with VSG that has style violations — violations are fixed (not silently discarded)
  • Format a VHDL file that is already compliant — no error, file unchanged
  • On Linux/Windows — existing bundled binary paths and formatter behaviour unchanged

The teroshdl.verible.restart command was registered twice: once inside
the Verilbe_lsp constructor and again as a fallback in configure_verilog()
when the language server was not found. This caused the extension to fail
to activate with "command already exists" on every startup.

Remove the redundant registration from configure_verilog().
…uage servers

vhdl-ls (rust_hdl.ts):
- Replace the hardcoded Linux binary name with platform/arch detection,
  adding proper support for vhdl_ls-aarch64-apple-darwin (Apple Silicon)
  and vhdl_ls-x86_64-apple-darwin (Intel Mac).
- Move the binary liveness check before the language client is created,
  so a missing or non-executable bundled binary is caught early.
- If the bundled binary fails, fall back to system-installed vhdl_ls at
  /opt/homebrew/bin, /usr/local/bin, and PATH.

verible (verible.ts):
- Apply the same early liveness check + system fallback pattern:
  tries /opt/homebrew/bin/verible-verilog-ls, /usr/local/bin/verible-verilog-ls,
  then the bare command from PATH.
- Remove stale commented-out middleware block.

Both changes make the extension usable on macOS where bundled binaries may
be missing or built for a different architecture.
Add the vhdl_ls-aarch64-apple-darwin binary and its accompanying
vhdl_libraries, matching the structure already in place for Windows
and Linux. This makes the extension work out-of-the-box on Macs with
Apple Silicon without requiring a separate vhdl-ls installation.
When vhdl-ls is not installed system-wide, it searches for the standard
VHDL libraries (IEEE, std, etc.) relative to its own binary at hardcoded
paths like ../vhdl_libraries and /usr/lib/rust_hdl/vhdl_libraries. These
paths resolve correctly on Linux but not on macOS, causing vhdl-ls to
panic on startup with "Couldn't find installed libraries".

Fix: compute the path of the bundled vhdl_libraries/vhdl_ls.toml at
startup and pass it via --libraries when it exists, making vhdl-ls
always able to locate the IEEE standard libraries.
…flag

vhdl_ls 0.84.0's --libraries flag expects a path to the DIRECTORY
containing vhdl_ls.toml, not the path to the file itself. Passing the
file path caused "Not a directory (os error 20)" which prevented the
standard IEEE/std libraries from loading, ultimately crashing the server
when analyzing any VHDL file.

Also fix get_toml() to remap the reserved 'work' library name to
teroshdlDefault, since vhdl_ls 0.84.0 rejects 'work' as an explicit
library name in the project configuration.
…th corruption

context.asAbsolutePath() uses path.join() internally, which prepends the
extension path to any string — including absolute system paths like
/opt/homebrew/bin/vhdl_ls, corrupting them to
<extension-dir>/opt/homebrew/bin/vhdl_ls (which doesn't exist).

Fix: store the already-resolved absolute path in the languageServer
module variable from the start. getServerOptionsEmbedded now uses it
directly without calling asAbsolutePath, making the system binary
fallback actually work correctly.

Same fix applied to verible.ts.
…ontent

VSG formatter (vsg.ts): The 63e925f commit broke formatting by treating
VSG exit code 1 as a failure. VSG exits with 1 when violations are found
and fixed (--fix mode) — the normal formatting outcome. Only codes > 1
indicate real errors. Result: edits were never applied when VSG actually
did formatting work.

teroshdl.ts: Initialize the VHDL_LS_CONFIG temp file with '[libraries]\n'
instead of an empty string. An empty file causes vhdl_ls to report
"missing field libraries" and then fall through to search the workspace
root for vhdl_ls.toml (which may have invalid content like 'work' library).
…startup

- Pass ...process.env to vhdl_ls server options so it inherits PATH, HOME,
  TMPDIR and other OS-level env vars; previously only VHDL_LS_CONFIG was
  passed, stripping the entire environment and causing unexpected crashes.

- Remove redundant explicit forceRefresh() call in init_tree_views():
  Tree_view_manager's constructor already emits GLOBAL_REFRESH which
  synchronously triggers refreshToml() → forceRefresh() → restart.
  The duplicate call was scheduling a second concurrent restart, causing
  an extra SIGKILL cycle against the freshly-started language server.
…rors

child_process.exec() provides the real exit code via error.code on the
ExecException object. The previous code unconditionally set return_value
to -1 for any non-zero exit, making it impossible for callers to
distinguish between exit 1 (VSG: violations found and fixed) and exit 2+
(VSG: real error).

VSG exits with 1 when --fix successfully reformats the file, so the VSG
formatter's check `return_value === 1` was never matching and every file
except already-clean ones was silently returning no edits.
The edalize ghdl backend assembles the ghdl -i command by joining file
paths with spaces (" ".join(files)) and writes them verbatim into the
generated Makefile.  When any source path contains a space the shell
splits the path into separate tokens and GHDL fails:

  ghdl:error: cannot open /Users/.../Sistemas
  make: *** [work-obj08.cf] Error 1

After backend.configure() writes the Makefile, quote_paths_with_spaces_in_makefile()
re-reads it and wraps every path that contains a space in double quotes.
The set of paths to fix is derived directly from the EDAM so only known
file names are touched; projects without spaces in their paths are
unaffected.  The function is wrapped in a try/except so a failure during
patching never blocks the actual build.
Make and the shell require different escaping for paths that contain
spaces:

- Recipe lines (tab-indented, executed by the shell) need double-quote
  wrapping: ghdl -i ... "/path/with spaces/file.vhd"
- Make variable definitions and dependency rules use Make's own word-
  splitting which does not respect shell quoting; spaces must be
  backslash-escaped: VHDL_SOURCES = /path/with\ spaces/file.vhd

The previous fix only applied shell double-quotes everywhere, which
fixed the ghdl -i recipe but caused Make itself to fail when expanding
$(VHDL_SOURCES) in dependency rules:
  make: No rule to make target '"path/with', needed by 'tb_pract4'

Now process the Makefile line-by-line, applying the correct escaping
strategy based on whether each line is a recipe or a Make directive.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant