Skip to content

Commit 429936d

Browse files
LSP Downloader (#1738)
<!-- ELLIPSIS_HIDDEN --> > [!IMPORTANT] > This PR adds BAML CLI version management, ensuring consistent generator versions and enabling CLI downloads with backoff logic. > > - **Behavior**: > - Adds `get_common_generator_version()` in `baml_project/mod.rs` to ensure all generators use the same major.minor version. > - Implements CLI version management in `cliDownloader.ts`, including download, extraction, and backoff logic. > - Updates `server.ts` to handle generator version mismatches and notify clients. > - **Notifications**: > - Adds `baml_src_version` notification in `notifications.rs` to communicate generator version to the client. > - Handles `baml_src_generator_version` in `index.ts` to update or restart the language server. > - **Misc**: > - Adds `semver` dependency in `Cargo.toml` for version parsing. > - Introduces `prep-debug-download.sh` for preparing CLI binaries for debugging. > - Updates `package.json` with new dependencies for CLI management. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for 25985f7. You can [customize](https://app.ellipsis.dev/BoundaryML/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN --> --------- Co-authored-by: Aaron Villalpando <aaron@boundaryml.com>
1 parent 291c10b commit 429936d

24 files changed

Lines changed: 1460 additions & 430 deletions

File tree

engine/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

engine/language_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ serde = { version = "1.0.197", features = ["derive"] }
3535
serde_json = { version = "1.0.113" }
3636
tracing-log = "0.2.0"
3737
shellexpand = { version = "3.0.0" }
38+
semver = "1.0.20"
3839

3940
thiserror = { version = "2.0.0" }
4041
baml-log = { path = "../baml-lib/baml-log" }

engine/language_server/src/baml_project/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use lsp_types::{
2323
Diagnostic, DiagnosticSeverity, Hover, HoverContents, Position, Range, TextDocumentItem,
2424
};
2525
use position_utils::get_word_at_position;
26+
use semver::Version;
2627
// use rustc_hash::FxHashSet;
2728
use std::collections::{hash_map::DefaultHasher, HashMap};
2829
use std::io;
@@ -1245,6 +1246,83 @@ impl Project {
12451246
// self.run_generators_without_debounce(on_success, on_error)
12461247
// .await;
12471248
// }
1249+
1250+
/// Checks if all generators use the same major.minor version.
1251+
/// Returns Ok(()) if they do (or if there are no generators),
1252+
/// otherwise returns an Err with a descriptive message.
1253+
pub fn get_common_generator_version(&self) -> Result<String, String> {
1254+
let runtime_version = env!("CARGO_PKG_VERSION");
1255+
1256+
let generators = match self.list_generators() {
1257+
Ok(gens) => gens,
1258+
Err(_) => return Ok(runtime_version.to_string()), // Return cargo pkg version if error listing generators
1259+
};
1260+
1261+
if generators.is_empty() {
1262+
return Ok(runtime_version.to_string());
1263+
}
1264+
1265+
let mut major_minor_versions = std::collections::HashMap::new();
1266+
let mut highest_patch_by_major_minor = std::collections::HashMap::new();
1267+
1268+
// Track major.minor versions and find highest patch for each
1269+
for gen in &generators {
1270+
if let Ok(version) = semver::Version::parse(&gen.version) {
1271+
let major_minor = format!("{}.{}", version.major, version.minor);
1272+
1273+
// Track generators with this major.minor
1274+
major_minor_versions
1275+
.entry(major_minor.clone())
1276+
.or_insert_with(Vec::new)
1277+
.push(gen.clone());
1278+
1279+
// Track highest patch version for this major.minor
1280+
highest_patch_by_major_minor
1281+
.entry(major_minor)
1282+
.and_modify(|highest_patch: &mut u64| {
1283+
if version.patch > *highest_patch {
1284+
*highest_patch = version.patch;
1285+
}
1286+
})
1287+
.or_insert(version.patch);
1288+
} else {
1289+
tracing::warn!("Invalid semver version in generator: {}", gen.version);
1290+
// Consider how to handle invalid versions - for now, we ignore them for the check
1291+
}
1292+
}
1293+
1294+
// If there's more than one major.minor version, return an error
1295+
if major_minor_versions.len() > 1 {
1296+
let versions_str = major_minor_versions
1297+
.keys()
1298+
.map(|v| format!("'{}'", v))
1299+
.collect::<Vec<_>>()
1300+
.join(", ");
1301+
1302+
let message = format!(
1303+
"Multiple generator major.minor versions detected: {}. Major and minor versions must match across all generators.",
1304+
versions_str
1305+
);
1306+
Err(message)
1307+
// If there's only one major.minor version, return it with the highest patch
1308+
} else if let Some((version, _)) = major_minor_versions.iter().next() {
1309+
if let Some(highest_patch) = highest_patch_by_major_minor.get(version) {
1310+
// Parse the version string to create a proper semver::Version
1311+
if let Ok(mut v) = Version::parse(&format!("{}.0", version)) {
1312+
// Update with the highest patch version
1313+
v.patch = *highest_patch;
1314+
Ok(v.to_string())
1315+
} else {
1316+
Ok(format!("{}.{}", version, highest_patch))
1317+
}
1318+
} else {
1319+
Ok(version.clone())
1320+
}
1321+
// Fallback to the runtime version if no valid versions were found
1322+
} else {
1323+
Err("No valid generator versions found".to_string())
1324+
}
1325+
}
12481326
}
12491327

12501328
fn get_dummy_value(

engine/language_server/src/server/api/diagnostics.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,51 @@ pub fn project_diagnostics(
244244
}
245245
}
246246

247+
// Check for generator version mismatch as well.
248+
if let Err(message) = guard.get_common_generator_version() {
249+
// Add the diagnostic to all generators
250+
if let Ok(generators) = guard.list_generators() {
251+
// Need to list generators again to get their spans
252+
for gen in &generators {
253+
if let Some(range) = span_to_range(
254+
&guard,
255+
&root_path,
256+
&Span {
257+
file: SourceFile::new_static(PathBuf::from(gen.span.file_path.clone()), ""),
258+
start: gen.span.start,
259+
end: gen.span.end,
260+
},
261+
) {
262+
let diagnostic = Diagnostic {
263+
range,
264+
message: message.clone(),
265+
severity: Some(DiagnosticSeverity::ERROR),
266+
source: Some("baml".to_string()),
267+
..Default::default()
268+
};
269+
270+
let span_path =
271+
ensure_absolute(&root_path, &PathBuf::from(gen.span.file_path.clone()));
272+
273+
match Url::from_file_path(span_path) {
274+
Ok(uri) => {
275+
diagnostics_map
276+
.entry(uri)
277+
.or_insert_with(Vec::new)
278+
.push(diagnostic);
279+
}
280+
Err(_) => {
281+
tracing::error!(
282+
"Failed to parse URI for version mismatch diagnostic: {}",
283+
gen.span.file_path
284+
);
285+
}
286+
}
287+
}
288+
}
289+
}
290+
}
291+
247292
diagnostics_map
248293
}
249294

engine/language_server/src/server/api/notifications.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod did_change;
33
mod did_change_configuration;
44
mod did_change_watched_files;
55
// mod did_change_workspace;
6+
mod baml_src_version;
67
mod did_close;
78
mod did_open;
89
mod did_save_text_document;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Serialize, Deserialize)]
4+
pub struct BamlSrcVersionPayload {
5+
pub version: String,
6+
pub root_path: String,
7+
}

engine/language_server/src/server/api/notifications/did_open.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use lsp_types::notification::DidOpenTextDocument;
22
use lsp_types::{DidOpenTextDocumentParams, PublishDiagnosticsParams, TextDocumentItem};
33

44
use crate::server::api::diagnostics::publish_session_lsp_diagnostics;
5+
use crate::server::api::notifications::baml_src_version::BamlSrcVersionPayload;
56
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
67
use crate::server::api::ResultExt;
78
use crate::server::client::{Notifier, Requester};
@@ -34,6 +35,28 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
3435
if project.is_none() {
3536
tracing::error!("Failed to get or create project for path: {:?}", file_path);
3637
show_err_msg!("Failed to get or create project for path: {:?}", file_path);
38+
} else {
39+
let project = project.unwrap();
40+
let version = project.lock().unwrap().get_common_generator_version();
41+
if let Ok(version) = version {
42+
notifier
43+
.0
44+
.send(lsp_server::Message::Notification(
45+
lsp_server::Notification::new(
46+
"baml_src_generator_version".to_string(),
47+
BamlSrcVersionPayload {
48+
version,
49+
root_path: project
50+
.lock()
51+
.unwrap()
52+
.root_path()
53+
.to_string_lossy()
54+
.to_string(),
55+
},
56+
),
57+
))
58+
.internal_error()?;
59+
}
3760
}
3861
// session.open_text_document(
3962
// DocumentKey::from_path(&file_path, &file_path).internal_error()?,

engine/language_server/src/server/api/notifications/did_save_text_document.rs

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::server::api::notifications::baml_src_version::BamlSrcVersionPayload;
12
use crate::server::api::ResultExt;
23
use crate::server::client::{Notifier, Requester};
34
use crate::server::Result;
@@ -27,25 +28,43 @@ impl super::SyncNotificationHandler for DidSaveTextDocument {
2728
session.clear_unsaved_files();
2829
session.reload(Some(notifier.clone())).internal_error()?;
2930
tracing::info!("About to run generator. URL path: {:?}", path);
30-
session
31+
let project = session
3132
.get_or_create_project(&path)
32-
.expect("Ensured that a project db exists")
33-
.lock()
34-
.unwrap()
35-
.run_generators_without_debounce(
36-
|message| {
37-
tracing::info!("About to notify client that generator has run.");
38-
notifier
39-
.notify_baml_info(&format!("{}", message))
40-
.unwrap_or(())
41-
},
42-
|e| {
43-
tracing::error!("Error generating: {e}");
44-
notifier
45-
.notify_baml_error(&format!("Error generating: {e}"))
46-
.unwrap_or(())
47-
},
48-
);
33+
.expect("Ensured that a project db exists");
34+
35+
let version = project.lock().unwrap().get_common_generator_version();
36+
if let Ok(version) = version {
37+
let _ = notifier.0.send(lsp_server::Message::Notification(
38+
lsp_server::Notification::new(
39+
"baml_src_generator_version".to_string(),
40+
BamlSrcVersionPayload {
41+
version,
42+
root_path: project
43+
.lock()
44+
.unwrap()
45+
.root_path()
46+
.to_string_lossy()
47+
.to_string(),
48+
},
49+
),
50+
));
51+
}
52+
53+
project.lock().unwrap().run_generators_without_debounce(
54+
|message| {
55+
tracing::info!("About to notify client that generator has run.");
56+
notifier
57+
.notify_baml_info(&format!("{}", message))
58+
.unwrap_or(())
59+
},
60+
|e| {
61+
tracing::error!("Error generating: {e}");
62+
notifier
63+
.notify_baml_error(&format!("Error generating: {e}"))
64+
.unwrap_or(())
65+
},
66+
);
67+
4968
Ok(())
5069
}
5170
}

integ-tests/baml_src/generators.baml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ generator lang_typescript {
99
output_dir "../typescript"
1010
version "0.85.0"
1111
}
12-
12+
1313

1414
generator lang_typescript_esm {
1515
output_type typescript
@@ -44,3 +44,7 @@ generator lang_go {
4444
version "0.85.0"
4545
client_package_name "example.com/integ-tests"
4646
}
47+
48+
49+
50+

integ-tests/go/baml_client/inlinedbaml.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)