diff --git a/.gitignore b/.gitignore
index e10bb0704..c7021dcb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,12 @@
-target
+build/
+target/
bin/
.DS_Store
*.msix
-# Node.js generated files for tree-sitter
-build/
-node_modules/
+# Generated files for tree-sitter
grammars/**/bindings/
grammars/**/src/
grammars/**/parser.*
+tree-sitter-ssh-server-config/
+tree-sitter-dscexpression/
diff --git a/.pipelines/DSC-Official.yml b/.pipelines/DSC-Official.yml
index dd04a277c..6ed3e6035 100644
--- a/.pipelines/DSC-Official.yml
+++ b/.pipelines/DSC-Official.yml
@@ -321,7 +321,7 @@ extends:
ob_restore_phase: true
- pwsh: |
apt update
- apt -y install musl-tools rpm dpkg build-essential
+ apt -y install musl-tools rpm dpkg build-essential protobuf-compiler
$header = "Bearer $(AzToken)"
$env:CARGO_REGISTRIES_POWERSHELL_TOKEN = $header
$env:CARGO_REGISTRIES_POWERSHELL_CREDENTIAL_PROVIDER = 'cargo:token'
@@ -381,6 +381,7 @@ extends:
apt -y install rpm
apt -y install dpkg
apt -y install build-essential
+ apt install -y protobuf-compiler
msrustup default stable-aarch64-unknown-linux-musl
if ((openssl version -d) -match 'OPENSSLDIR: "(?
.*?)"') {
$env:OPENSSL_LIB_DIR = $matches['dir']
diff --git a/Cargo.lock b/Cargo.lock
index a5462091c..615a40c8f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -96,6 +96,12 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
[[package]]
name = "arc-swap"
version = "1.7.1"
@@ -108,6 +114,28 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -131,6 +159,49 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "axum"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+]
+
[[package]]
name = "base32"
version = "0.5.1"
@@ -855,6 +926,24 @@ dependencies = [
"tree-sitter-ssh-server-config",
]
+[[package]]
+name = "dscbicep"
+version = "0.1.0"
+dependencies = [
+ "async-stream",
+ "clap",
+ "dsc-lib",
+ "prost",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tonic-prost",
+ "tonic-prost-build",
+ "tonic-reflection",
+ "tracing",
+ "tracing-subscriber",
+]
+
[[package]]
name = "dsctest"
version = "0.1.0"
@@ -948,6 +1037,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
[[package]]
name = "flate2"
version = "1.1.4"
@@ -1162,6 +1257,25 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
[[package]]
name = "hashbrown"
version = "0.16.0"
@@ -1219,6 +1333,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
[[package]]
name = "hyper"
version = "1.7.0"
@@ -1229,9 +1349,11 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
+ "h2",
"http",
"http-body",
"httparse",
+ "httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
@@ -1257,6 +1379,19 @@ dependencies = [
"webpki-roots",
]
+[[package]]
+name = "hyper-timeout"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
+dependencies = [
+ "hyper",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
[[package]]
name = "hyper-util"
version = "0.1.17"
@@ -1613,12 +1748,24 @@ dependencies = [
"regex-automata",
]
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -1641,6 +1788,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "multimap"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+
[[package]]
name = "murmurhash64"
version = "0.3.1"
@@ -2050,6 +2203,36 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+[[package]]
+name = "petgraph"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -2115,6 +2298,16 @@ dependencies = [
"yansi",
]
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.103"
@@ -2124,6 +2317,80 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "prost"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
+dependencies = [
+ "heck",
+ "itertools",
+ "log",
+ "multimap",
+ "once_cell",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "pulldown-cmark",
+ "pulldown-cmark-to-cmark",
+ "regex",
+ "syn",
+ "tempfile",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "pulldown-cmark"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
+dependencies = [
+ "bitflags 2.9.4",
+ "memchr",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-to-cmark"
+version = "21.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1"
+dependencies = [
+ "pulldown-cmark",
+]
+
[[package]]
name = "quick-xml"
version = "0.38.3"
@@ -3111,6 +3378,17 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "tokio-util"
version = "0.7.16"
@@ -3165,6 +3443,88 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+[[package]]
+name = "tonic"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
+dependencies = [
+ "async-trait",
+ "axum",
+ "base64",
+ "bytes",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-timeout",
+ "hyper-util",
+ "percent-encoding",
+ "pin-project",
+ "socket2",
+ "sync_wrapper",
+ "tokio",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tonic-prost"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
+dependencies = [
+ "bytes",
+ "prost",
+ "tonic",
+]
+
+[[package]]
+name = "tonic-prost-build"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "prost-types",
+ "quote",
+ "syn",
+ "tempfile",
+ "tonic-build",
+]
+
+[[package]]
+name = "tonic-reflection"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34da53e8387581d66db16ff01f98a70b426b091fdf76856e289d5c1bd386ed7b"
+dependencies = [
+ "prost",
+ "prost-types",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tonic-prost",
+]
+
[[package]]
name = "tower"
version = "0.5.2"
@@ -3173,11 +3533,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
+ "indexmap",
"pin-project-lite",
+ "slab",
"sync_wrapper",
"tokio",
+ "tokio-util",
"tower-layer",
"tower-service",
+ "tracing",
]
[[package]]
@@ -3369,6 +3733,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
[[package]]
name = "unicode-general-category"
version = "1.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 7099b4d29..ca8671231 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ resolver = "2"
# the path to a crate.
members = [
"dsc",
+ "dscbicep",
"lib/dsc-lib",
"lib/dsc-lib-jsonschema",
"lib/dsc-lib-jsonschema-macros",
@@ -28,6 +29,7 @@ members = [
# avoid unintentionally modifying this value.
default-members = [
"dsc",
+ "dscbicep",
"lib/dsc-lib",
"lib/dsc-lib-jsonschema",
"lib/dsc-lib-jsonschema-macros",
@@ -54,6 +56,7 @@ default-members = [
# current operating system to enable faster builds.
Windows = [
"dsc",
+ "dscbicep",
"lib/dsc-lib",
"lib/dsc-lib-jsonschema",
"lib/dsc-lib-jsonschema-macros",
@@ -75,6 +78,7 @@ Windows = [
]
macOS = [
"dsc",
+ "dscbicep",
"lib/dsc-lib",
"lib/dsc-lib-jsonschema",
"lib/dsc-lib-jsonschema-macros",
@@ -93,6 +97,7 @@ macOS = [
]
Linux = [
"dsc",
+ "dscbicep",
"lib/dsc-lib",
"lib/dsc-lib-jsonschema",
"lib/dsc-lib-jsonschema-macros",
@@ -127,7 +132,7 @@ base32 = { version = "0.5" }
base64 = { version = "0.22" }
# dsc-lib, sshdconfig
chrono = { version = "0.4" }
-# dsc, dsc-lib, dscecho, registry, runcommandonset, sshdconfig, dsctest, test_group_resource
+# dsc, dsc-lib, dscbicep, dscecho, registry, runcommandonset, sshdconfig, dsctest, test_group_resource
clap = { version = "4.5", features = ["derive"] }
# dsc
clap_complete = { version = "4.5" }
@@ -141,6 +146,8 @@ darling = { version = "0.23" }
derive_builder = { version = "0.20" }
# dsc, dsc-lib
indicatif = { version = "0.18" }
+# dscbicep
+async-stream = { version = "0.3" }
# dsc-lib-security_context::windows
is_elevated = { version = "0.1" }
# dsc, dsc-lib
@@ -157,6 +164,8 @@ num-traits = { version = "0.2" }
os_info = { version = "3.14" }
# dsc, dsc-lib
path-absolutize = { version = "3.1" }
+# dscbicep
+prost = { version = "0.14" }
# dsc-lib-jsonschema-macros
proc-macro2 = { version = "1.0" }
# dsc-lib-jsonschema-macros
@@ -191,15 +200,23 @@ sysinfo = { version = "0.37" }
tempfile = { version = "3.24" }
# dsc, dsc-lib, registry, dsc-lib-registry, sshdconfig
thiserror = { version = "2.0" }
-# dsc, dsc-lib
+# dsc, dsc-lib, dscbicep
tokio = { version = "1.49" }
+# dscbicep
+tokio-stream = { version = "0.1" }
# dsc
tokio-util = { version = "0.7" }
-# dsc, dsc-lib, registry, dsc-lib-registry, runcommandonset, sshdconfig
+# dscbicep
+tonic = { version = "0.14" }
+# dscbicep
+tonic-prost = { version = "0.14" }
+# dscbicep
+tonic-reflection = { version = "0.14" }
+# dsc, dsc-lib, dscbicep, registry, dsc-lib-registry, runcommandonset, sshdconfig
tracing = { version = "0.1" }
# dsc, dsc-lib
tracing-indicatif = { version = "0.3" }
-# dsc, registry, dsc-lib-registry, runcommandonset, sshdconfig
+# dsc, dscbicep, registry, dsc-lib-registry, runcommandonset, sshdconfig
tracing-subscriber = { version = "0.3", features = ["ansi", "env-filter", "json"] }
# dsc-lib, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config
tree-sitter = { version = "0.26" }
@@ -223,6 +240,8 @@ ipnetwork = { version = "0.21" }
# build-only dependencies
# dsc-lib, dsc-lib-registry, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config
cc = { version = "1.2" }
+# dsc
+tonic-prost-build = { version = "0.14" }
# test-only dependencies
# dsc-lib-jsonschema
diff --git a/build.helpers.psm1 b/build.helpers.psm1
index ab32cd4ec..5a09b3be5 100644
--- a/build.helpers.psm1
+++ b/build.helpers.psm1
@@ -393,7 +393,7 @@ function Install-Rust {
param()
process {
- if ((Test-CommandAvailable -Name 'cargo')) {
+ if (Test-CommandAvailable -Name 'cargo') {
Write-Verbose "Rust already installed"
return
}
@@ -597,7 +597,7 @@ function Install-NodeJS {
param()
process {
- if ((Get-Command 'node' -ErrorAction Ignore)) {
+ if (Test-CommandAvailable -Name 'node') {
Write-Verbose "Node.js already installed."
return
}
@@ -611,7 +611,6 @@ function Install-NodeJS {
}
} elseif ($IsWindows) {
if (Get-Command 'winget' -ErrorAction Ignore) {
- Write-Warning "WHY WHAT IS HAPPENING HERE"
Write-Verbose -Verbose "Using winget to install Node.js"
winget install OpenJS.NodeJS --accept-source-agreements --accept-package-agreements --source winget --silent
} else {
@@ -627,6 +626,50 @@ function Install-NodeJS {
}
}
+function Install-Protobuf {
+ <#
+ .SYNOPSIS
+ Installs Protobuf for the protoc executable.
+ #>
+
+ [cmdletbinding()]
+ param()
+
+ process {
+ if (Test-CommandAvailable -Name 'protoc') {
+ Write-Verbose "Protobuf already installed."
+ return
+ }
+
+ Write-Verbose -Verbose "Protobuf not found, installing..."
+ if ($IsMacOS) {
+ if (Get-Command 'brew' -ErrorAction Ignore) {
+ brew install protobuf
+ } else {
+ Write-Warning "Homebrew not found, please install Protobuf manually"
+ }
+ } elseif ($IsWindows) {
+ if (Get-Command 'winget' -ErrorAction Ignore) {
+ Write-Verbose -Verbose "Using winget to install Protobuf"
+ winget install Google.Protobuf --accept-source-agreements --accept-package-agreements --source winget --silent
+ } else {
+ Write-Warning "winget not found, please install Protobuf manually"
+ }
+ } else {
+ if (Get-Command 'apt' -ErrorAction Ignore) {
+ Write-Verbose -Verbose "Using apt to install Protobuf"
+ sudo apt install -y protobuf-compiler
+ } else {
+ Write-Warning "apt not found, please install Protobuf manually"
+ }
+ }
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to install Protobuf"
+ }
+ }
+}
+
function Install-PowerShellTestPrerequisite {
[cmdletbinding()]
param(
diff --git a/build.ps1 b/build.ps1
index ffbc72ac5..a293282b5 100755
--- a/build.ps1
+++ b/build.ps1
@@ -203,13 +203,14 @@ process {
}
if (-not ($SkipBuild -and $Test -and $ExcludeRustTests)) {
- # Install Node if needed
Write-BuildProgress @progressParams -Status 'Ensuring Node.JS is available'
Install-NodeJS @VerboseParam
-
- # Ensure tree-sitter is installed
+
Write-BuildProgress @progressParams -Status 'Ensuring tree-sitter is available'
Install-TreeSitter -UseCFS:$UseCFS @VerboseParam
+
+ Write-BuildProgress @progressParams -Status 'Ensuring Protobuf is available'
+ Install-Protobuf @VerboseParam
}
}
@@ -259,7 +260,7 @@ process {
if ($Test) {
$progressParams.Activity = 'Testing projects'
Write-BuildProgress @progressParams
-
+
if (-not $ExcludeRustTests) {
$rustTestParams = @{
Project = $BuildData.Projects
@@ -319,4 +320,4 @@ clean {
}
Write-BuildProgress -Completed
-}
\ No newline at end of file
+}
diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml
index 61d5946b6..62d3525b2 100644
--- a/dsc/locales/en-us.toml
+++ b/dsc/locales/en-us.toml
@@ -36,6 +36,7 @@ functionAbout = "Operations on DSC functions"
listFunctionAbout = "List or find functions"
version = "The version of the resource to invoke in semver format"
mcpAbout = "Use DSC as a MCP server"
+bicepAbout = "Use DSC as a Bicep server over gRPC"
[main]
ctrlCReceived = "Ctrl-C received"
diff --git a/dsc/src/util.rs b/dsc/src/util.rs
index 6e4ec31a0..049a886fa 100644
--- a/dsc/src/util.rs
+++ b/dsc/src/util.rs
@@ -73,6 +73,7 @@ pub const EXIT_CTRL_C: i32 = 6;
pub const EXIT_DSC_RESOURCE_NOT_FOUND: i32 = 7;
pub const EXIT_DSC_ASSERTION_FAILED: i32 = 8;
pub const EXIT_MCP_FAILED: i32 = 9;
+pub const EXIT_BICEP_FAILED: i32 = 10;
pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT";
pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL";
diff --git a/dscbicep/.project.data.json b/dscbicep/.project.data.json
new file mode 100644
index 000000000..1f0ee7c11
--- /dev/null
+++ b/dscbicep/.project.data.json
@@ -0,0 +1,6 @@
+{
+ "Name": "dscbicep",
+ "Kind": "CLI",
+ "IsRust": true,
+ "Binaries": ["dscbicep"]
+}
diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml
new file mode 100644
index 000000000..2140e2900
--- /dev/null
+++ b/dscbicep/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "dscbicep"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "dscbicep"
+path = "src/main.rs"
+
+[dependencies]
+dsc-lib = { workspace = true }
+
+async-stream = { workspace = true }
+clap = { workspace = true }
+prost = { workspace = true }
+tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std", "net"] }
+tokio-stream = { workspace = true, features = ["net"] }
+tonic = { workspace = true }
+tonic-prost = { workspace = true }
+tonic-reflection = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
+
+[build-dependencies]
+tonic-prost-build = { workspace = true }
diff --git a/dscbicep/build.rs b/dscbicep/build.rs
new file mode 100644
index 000000000..ae8c430da
--- /dev/null
+++ b/dscbicep/build.rs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::{env, path::PathBuf};
+
+fn main() -> Result<(), Box> {
+ let descriptor_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bicep.bin");
+
+ tonic_prost_build::configure()
+ .build_client(false)
+ .file_descriptor_set_path(&descriptor_path)
+ .compile_protos(&["proto/bicep.proto"], &["proto"])?;
+ Ok(())
+}
diff --git a/dscbicep/proto/bicep.proto b/dscbicep/proto/bicep.proto
new file mode 100644
index 000000000..c02b28d90
--- /dev/null
+++ b/dscbicep/proto/bicep.proto
@@ -0,0 +1,66 @@
+syntax = "proto3";
+
+option csharp_namespace = "Bicep.Local.Rpc";
+
+package extension;
+
+service BicepExtension {
+ rpc CreateOrUpdate (ResourceSpecification) returns (LocalExtensibilityOperationResponse);
+ rpc Preview (ResourceSpecification) returns (LocalExtensibilityOperationResponse);
+ rpc Get (ResourceReference) returns (LocalExtensibilityOperationResponse);
+ rpc Delete (ResourceReference) returns (LocalExtensibilityOperationResponse);
+ rpc GetTypeFiles(Empty) returns (TypeFilesResponse);
+ rpc Ping(Empty) returns (Empty);
+}
+
+message Empty {}
+
+message ResourceSpecification {
+ optional string config = 1;
+ string type = 2;
+ optional string apiVersion = 3;
+ string properties = 4;
+}
+
+message ResourceReference {
+ string identifiers = 1;
+ optional string config = 2;
+ string type = 3;
+ optional string apiVersion = 4;
+}
+
+message LocalExtensibilityOperationResponse {
+ optional Resource resource = 1;
+ optional ErrorData errorData = 2;
+}
+
+message Resource {
+ string type = 1;
+ optional string apiVersion = 2;
+ string identifiers = 3;
+ string properties = 4;
+ optional string status = 5;
+}
+
+message ErrorData {
+ Error error = 1;
+}
+
+message Error {
+ string code = 1;
+ optional string target = 2;
+ string message = 3;
+ repeated ErrorDetail details = 4;
+ optional string innerError = 5;
+}
+
+message ErrorDetail {
+ string code = 1;
+ optional string target = 2;
+ string message = 3;
+}
+
+message TypeFilesResponse {
+ string indexFile = 1;
+ map typeFiles = 2;
+}
diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs
new file mode 100644
index 000000000..862e49cc5
--- /dev/null
+++ b/dscbicep/src/main.rs
@@ -0,0 +1,425 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use clap::Parser;
+use dsc_lib::{
+ configure::config_doc::ExecutionKind,
+ discovery::discovery_trait::DiscoveryFilter,
+ dscresources::{
+ dscresource::Invoke,
+ invoke_result::{GetResult, SetResult},
+ },
+ DscManager,
+};
+use std::{env, io, process};
+use tonic::{transport::Server, Request, Response, Status};
+
+// Include the generated protobuf code
+pub mod proto {
+ tonic::include_proto!("extension");
+ pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("bicep");
+}
+
+use proto::bicep_extension_server::{BicepExtension, BicepExtensionServer};
+use proto::{
+ Empty, LocalExtensibilityOperationResponse, ResourceReference, ResourceSpecification,
+ TypeFilesResponse,
+};
+
+#[derive(Debug, Default)]
+pub struct BicepExtensionService;
+
+#[tonic::async_trait]
+impl BicepExtension for BicepExtensionService {
+ async fn create_or_update(
+ &self,
+ request: Request,
+ ) -> Result, Status> {
+ let spec = request.into_inner();
+ let resource_type = spec.r#type;
+ let version = spec.api_version;
+ let properties = spec.properties;
+
+ tracing::debug!("CreateOrUpdate called for {resource_type}@{version:?}: {properties}");
+
+ let mut dsc = DscManager::new();
+ let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else {
+ return Err(Status::not_found("Resource not found"));
+ };
+
+ let SetResult::Resource(result) = resource
+ .set(&properties, false, &ExecutionKind::Actual)
+ .map_err(|e| Status::aborted(e.to_string()))?
+ else {
+ return Err(Status::unimplemented("Group resources not supported"));
+ };
+
+ Ok(Response::new(LocalExtensibilityOperationResponse {
+ resource: Some(proto::Resource {
+ r#type: resource_type,
+ api_version: version,
+ identifiers: properties,
+ properties: result.after_state.to_string(),
+ status: None,
+ }),
+ error_data: None,
+ }))
+ }
+
+ async fn preview(
+ &self,
+ request: Request,
+ ) -> Result, Status> {
+ let spec = request.into_inner();
+ let resource_type = spec.r#type;
+ let version = spec.api_version;
+ let properties = spec.properties;
+
+ tracing::debug!("Preview called for {resource_type}@{version:?}: {properties}");
+
+ let mut dsc = DscManager::new();
+ let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else {
+ return Err(Status::not_found("Resource not found"));
+ };
+
+ let SetResult::Resource(result) = resource
+ .set(&properties, false, &ExecutionKind::WhatIf)
+ .map_err(|e| Status::aborted(e.to_string()))?
+ else {
+ return Err(Status::unimplemented("Group resources not supported"));
+ };
+
+ Ok(Response::new(LocalExtensibilityOperationResponse {
+ resource: Some(proto::Resource {
+ r#type: resource_type,
+ api_version: version,
+ identifiers: properties,
+ properties: result.after_state.to_string(),
+ status: None,
+ }),
+ error_data: None,
+ }))
+ }
+
+ async fn get(
+ &self,
+ request: Request,
+ ) -> Result, Status> {
+ let reference = request.into_inner();
+ let resource_type = reference.r#type.clone();
+ let version = reference.api_version.clone();
+ let identifiers = reference.identifiers.clone();
+
+ tracing::debug!("Get called for {resource_type}@{version:?}: {identifiers}");
+
+ let mut dsc = DscManager::new();
+ let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else {
+ return Err(Status::not_found("Resource not found"));
+ };
+
+ // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep.
+ let GetResult::Resource(result) = resource
+ .get(&identifiers)
+ .map_err(|e| Status::aborted(e.to_string()))?
+ else {
+ return Err(Status::unimplemented("Group resources not supported"));
+ };
+
+ Ok(Response::new(LocalExtensibilityOperationResponse {
+ resource: Some(proto::Resource {
+ r#type: resource_type,
+ api_version: version,
+ identifiers: identifiers,
+ properties: result.actual_state.to_string(),
+ status: None,
+ }),
+ error_data: None,
+ }))
+ }
+
+ async fn delete(
+ &self,
+ request: Request,
+ ) -> Result, Status> {
+ let reference = request.into_inner();
+ let resource_type = reference.r#type.clone();
+ let version = reference.api_version.clone();
+ let identifiers = reference.identifiers.clone();
+
+ tracing::debug!(
+ "Delete called for {}@{:?}: {}",
+ resource_type,
+ version,
+ identifiers
+ );
+
+ let mut dsc = DscManager::new();
+ let Some(resource) = dsc.find_resource(&DiscoveryFilter::new(&resource_type, version.as_deref(), None)).unwrap_or(None) else {
+ return Err(Status::not_found("Resource not found"));
+ };
+
+ // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep.
+ resource
+ .delete(&identifiers)
+ .map_err(|e| Status::aborted(e.to_string()))?;
+
+ Ok(Response::new(LocalExtensibilityOperationResponse {
+ resource: Some(proto::Resource {
+ r#type: resource_type,
+ api_version: version,
+ identifiers: identifiers,
+ properties: "{}".to_string(),
+ status: None,
+ }),
+ error_data: None,
+ }))
+ }
+
+ async fn get_type_files(
+ &self,
+ _request: Request,
+ ) -> Result, Status> {
+ tracing::debug!("GetTypeFiles called");
+
+ // TODO: Return actual Bicep type definitions...yet the extension already has these?
+ // Perhaps this is where we can dynamically get them from the current system.
+ Err(Status::unimplemented("GetTypeFiles not yet implemented"))
+ }
+
+ async fn ping(&self, _request: Request) -> Result, Status> {
+ tracing::debug!("Ping called");
+ Ok(Response::new(Empty {}))
+ }
+}
+
+#[derive(Parser, Debug)]
+#[command(name = "dscbicep")]
+#[command(about = "DSC Bicep Local Deploy Extension", long_about = None)]
+struct Args {
+ /// The path to the domain socket to connect on (Unix-like systems)
+ #[arg(long)]
+ socket: Option,
+
+ /// The named pipe to connect on (Windows)
+ #[arg(long)]
+ pipe: Option,
+
+ /// The HTTP address to listen on (e.g., 127.0.0.1:50051)
+ #[arg(long)]
+ http: Option,
+
+ /// Wait for debugger to attach before starting
+ #[arg(long)]
+ wait_for_debugger: bool,
+}
+
+#[allow(unused_variables)]
+async fn run_server(
+ socket: Option,
+ pipe: Option,
+ http: Option,
+) -> Result<(), Box> {
+ let service = BicepExtensionService;
+
+ #[cfg(unix)]
+ if let Some(socket_path) = socket {
+ use tokio::net::UnixListener;
+ use tokio_stream::wrappers::UnixListenerStream;
+
+ tracing::info!("Starting Bicep gRPC server on Unix socket: {socket_path}");
+
+ // Remove the socket file if it exists
+ let _ = std::fs::remove_file(&socket_path);
+
+ let uds = UnixListener::bind(&socket_path)?;
+ let uds_stream = UnixListenerStream::new(uds);
+
+ Server::builder()
+ .add_service(BicepExtensionServer::new(service))
+ .serve_with_incoming(uds_stream)
+ .await?;
+
+ return Ok(());
+ }
+
+ #[cfg(windows)]
+ if let Some(pipe_name) = pipe {
+ // TODO: This named pipe code is messy and honestly mostly generated. It
+ // does work, but most of the problem lies in minimal Windows support
+ // inside the Tokio library (and no support for UDS).
+ use std::pin::Pin;
+ use std::task::{Context, Poll};
+ use tokio::io::{AsyncRead, AsyncWrite};
+ use tokio::net::windows::named_pipe::ServerOptions;
+ use tonic::transport::server::Connected;
+
+ // Wrapper to implement Connected trait for NamedPipeServer
+ struct NamedPipeConnection(tokio::net::windows::named_pipe::NamedPipeServer);
+
+ impl Connected for NamedPipeConnection {
+ type ConnectInfo = ();
+
+ fn connect_info(&self) -> Self::ConnectInfo {
+ ()
+ }
+ }
+
+ impl AsyncRead for NamedPipeConnection {
+ fn poll_read(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &mut tokio::io::ReadBuf<'_>,
+ ) -> Poll> {
+ Pin::new(&mut self.0).poll_read(cx, buf)
+ }
+ }
+
+ impl AsyncWrite for NamedPipeConnection {
+ fn poll_write(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ buf: &[u8],
+ ) -> Poll> {
+ Pin::new(&mut self.0).poll_write(cx, buf)
+ }
+
+ fn poll_flush(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ ) -> Poll> {
+ Pin::new(&mut self.0).poll_flush(cx)
+ }
+
+ fn poll_shutdown(
+ mut self: Pin<&mut Self>,
+ cx: &mut Context<'_>,
+ ) -> Poll> {
+ Pin::new(&mut self.0).poll_shutdown(cx)
+ }
+ }
+
+ // Windows named pipes must be in the format \\.\pipe\{name}
+ let full_pipe_path = format!(r"\\.\pipe\{}", pipe_name);
+ tracing::info!("Starting Bicep gRPC server on named pipe: {full_pipe_path}");
+
+ // Create a stream that accepts connections on the named pipe
+ let incoming = async_stream::stream! {
+ // Track whether this is the first instance
+ let mut is_first = true;
+
+ loop {
+ let pipe = if is_first {
+ ServerOptions::new()
+ .first_pipe_instance(true)
+ .create(&full_pipe_path)
+ } else {
+ ServerOptions::new()
+ .create(&full_pipe_path)
+ };
+
+ let server = match pipe {
+ Ok(server) => server,
+ Err(e) => {
+ tracing::error!("Failed to create named pipe: {}", e);
+ break;
+ }
+ };
+
+ is_first = false;
+
+ tracing::debug!("Waiting for client to connect to named pipe...");
+ match server.connect().await {
+ Ok(()) => {
+ tracing::info!("Client connected to named pipe");
+ yield Ok::<_, std::io::Error>(NamedPipeConnection(server));
+ }
+ Err(e) => {
+ tracing::error!("Failed to accept connection: {}", e);
+ break;
+ }
+ }
+ }
+ };
+
+ Server::builder()
+ .add_service(BicepExtensionServer::new(service))
+ .serve_with_incoming(incoming)
+ .await?;
+
+ return Ok(());
+ }
+
+ // Default to HTTP server on [::1]:50051 if no transport specified
+ let addr = http.unwrap_or_else(|| "[::1]:50051".to_string()).parse()?;
+ tracing::info!("Starting Bicep gRPC server on HTTP: {addr}");
+
+ let reflection_service = tonic_reflection::server::Builder::configure()
+ .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET)
+ .build_v1()
+ .unwrap();
+
+ Server::builder()
+ .add_service(reflection_service)
+ .add_service(BicepExtensionServer::new(service))
+ .serve(addr)
+ .await?;
+
+ Ok(())
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let trace_level = env::var("DSC_TRACE_LEVEL")
+ .ok()
+ .and_then(|level| match level.to_uppercase().as_str() {
+ "TRACE" => Some(tracing::Level::TRACE),
+ "DEBUG" => Some(tracing::Level::DEBUG),
+ "INFO" => Some(tracing::Level::INFO),
+ "WARN" => Some(tracing::Level::WARN),
+ "ERROR" => Some(tracing::Level::ERROR),
+ _ => None,
+ })
+ .unwrap_or(tracing::Level::WARN);
+
+ tracing_subscriber::fmt()
+ .with_target(false)
+ .with_level(true)
+ .with_max_level(trace_level)
+ .init();
+
+ let args = Args::parse();
+ tracing::debug!("Args are {args:#?}");
+
+ if args.wait_for_debugger
+ || env::var_os("DSC_GRPC_DEBUG").is_some_and(|v| v.eq_ignore_ascii_case("true"))
+ {
+ tracing::warn!(
+ "Press any key to continue after attaching to PID: {}",
+ process::id()
+ );
+ let mut input = String::new();
+ io::stdin().read_line(&mut input)?;
+ }
+
+ // Set up graceful shutdown on SIGTERM/SIGINT
+ let shutdown_signal = async {
+ tokio::signal::ctrl_c()
+ .await
+ .expect("Failed to listen for shutdown signal");
+ tracing::info!("Received shutdown signal, terminating gracefully...");
+ };
+
+ tokio::select! {
+ result = run_server(args.socket, args.pipe, args.http) => {
+ if let Err(e) = result {
+ tracing::error!("Server error: {e}");
+ return Err(e);
+ }
+ }
+ _ = shutdown_signal => {
+ tracing::info!("Shutdown complete");
+ }
+ }
+
+ Ok(())
+}
diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs
index 082895ccf..b255f8255 100644
--- a/lib/dsc-lib/src/dscresources/command_resource.rs
+++ b/lib/dsc-lib/src/dscresources/command_resource.rs
@@ -765,24 +765,39 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option
let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?;
let executable = canonicalize_which(executable, cwd)?;
- tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(
- async {
- trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?}));
- if let Some(cwd) = cwd {
- trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd.display()));
- }
+ let run_async = async {
+ trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?}));
+ if let Some(cwd) = cwd {
+ trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd.display()));
+ }
- match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await {
- Ok((code, stdout, stderr)) => {
- Ok((code, stdout, stderr))
- },
- Err(err) => {
- error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err));
- Err(err)
- }
+ match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await {
+ Ok((code, stdout, stderr)) => {
+ Ok((code, stdout, stderr))
+ },
+ Err(err) => {
+ error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err));
+ Err(err)
}
}
- )
+ };
+
+ // Try to use existing runtime first (e.g. from gRPC or MCP server)
+ match tokio::runtime::Handle::try_current() {
+ Ok(handle) => {
+ tokio::task::block_in_place(|| {
+ handle.block_on(run_async)
+ })
+ },
+ // Otherwise create a new runtime
+ Err(_) => {
+ tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .unwrap()
+ .block_on(run_async)
+ }
+ }
}
/// Process the arguments for a command resource.