1use std::path::{Path, PathBuf};
4use std::sync::Once;
5
6use aube::cli_args::NetworkArgs;
7use aube::embed::{
8 self, AddToProjectOptions, DepSelection, EmbedderInstallOverrides, EmbedderRuntime, FrozenMode,
9 InstallControl, NetworkMode,
10};
11use tokio::sync::Mutex;
12
13use crate::error::{Error, Result};
14
15static OSDK_HOST: embed::Host = embed::Host {
16 name: "osdk",
17 display_name: "osdk",
18 vendor: None,
19 version: env!("CARGO_PKG_VERSION"),
20 user_agent: concat!("osdk/", env!("CARGO_PKG_VERSION")),
21 self_names: embed::AUBE.self_names,
24 compatible_names: embed::AUBE.compatible_names,
25 lockfile_basename: embed::AUBE.lockfile_basename,
26 workspace_yaml: embed::AUBE.workspace_yaml,
27 manifest_namespace: embed::AUBE.manifest_namespace,
28 env_prefix: None,
29 config_env_prefix: None,
30 cache_namespace: "osdk-aube",
31 data_namespace: "osdk-aube",
32 canonical_lockfile_always_wins: true,
33 runtime_switching: false,
34 self_engines_check: false,
35 self_update_enabled: false,
36};
37
38static INIT: Once = Once::new();
39static AUBE_OPERATION_LOCK: Mutex<()> = Mutex::const_new(());
44static AUBE_REGISTRY_OVERRIDE_INSTALLER: AubeRegistryOverrideInstaller =
45 AubeRegistryOverrideInstaller;
46
47trait RegistryOverrideInstaller {
48 fn install(&self, registry: Option<String>);
49}
50
51struct AubeRegistryOverrideInstaller;
52
53impl RegistryOverrideInstaller for AubeRegistryOverrideInstaller {
54 fn install(&self, registry: Option<String>) {
55 NetworkArgs {
56 registry,
57 ..Default::default()
58 }
59 .install_overrides();
60 }
61}
62
63struct RegistryOverrideReset<'a, I: RegistryOverrideInstaller + ?Sized> {
64 installer: &'a I,
65}
66
67impl<'a, I: RegistryOverrideInstaller + ?Sized> RegistryOverrideReset<'a, I> {
68 fn install(installer: &'a I, registry: Option<String>) -> Self {
69 installer.install(registry);
70 Self { installer }
71 }
72}
73
74impl<I: RegistryOverrideInstaller + ?Sized> Drop for RegistryOverrideReset<'_, I> {
75 fn drop(&mut self) {
76 self.installer.install(None);
77 }
78}
79
80async fn with_registry_override<T, F, Fut, I>(
81 operation_lock: &Mutex<()>,
82 installer: &I,
83 registry: Option<String>,
84 operation: F,
85) -> T
86where
87 F: FnOnce() -> Fut,
88 Fut: std::future::Future<Output = T>,
89 I: RegistryOverrideInstaller + ?Sized,
90{
91 let _operation_guard = operation_lock.lock().await;
92 let _reset = RegistryOverrideReset::install(installer, registry);
93 operation().await
94}
95
96async fn with_aube_registry_override<T, F, Fut>(registry: Option<String>, operation: F) -> T
97where
98 F: FnOnce() -> Fut,
99 Fut: std::future::Future<Output = T>,
100{
101 with_registry_override(
102 &AUBE_OPERATION_LOCK,
103 &AUBE_REGISTRY_OVERRIDE_INSTALLER,
104 registry,
105 operation,
106 )
107 .await
108}
109
110pub struct EmbeddedInstallRequest<'a> {
111 pub project_dir: &'a Path,
112 pub packages: &'a [String],
113 pub cache_dir: PathBuf,
114 pub store_dir: PathBuf,
115 pub node_bin_dir: PathBuf,
116 pub scripts_enabled: bool,
117 pub dangerously_allow_all_builds: bool,
118 pub offline: bool,
119 pub registry: Option<String>,
120}
121
122pub struct EmbeddedProjectAddRequest<'a> {
126 pub project_dir: &'a Path,
127 pub packages: &'a [String],
128 pub cache_dir: PathBuf,
129 pub store_dir: PathBuf,
130 pub node_bin_dir: PathBuf,
131 pub save_dev: bool,
132 pub save_optional: bool,
133 pub save_peer: bool,
134 pub offline: bool,
135 pub registry: Option<String>,
136}
137
138pub struct EmbeddedFrozenInstallRequest<'a> {
139 pub project_dir: &'a Path,
140 pub cache_dir: PathBuf,
141 pub store_dir: PathBuf,
142 pub node_bin_dir: PathBuf,
143 pub scripts_enabled: bool,
144 pub dangerously_allow_all_builds: bool,
145 pub offline: bool,
146 pub registry: Option<String>,
147}
148
149pub struct EmbeddedLockGraphRequest<'a> {
150 pub project_dir: &'a Path,
151 pub cache_dir: PathBuf,
152 pub store_dir: PathBuf,
153 pub node_bin_dir: PathBuf,
154 pub offline: bool,
155 pub registry: Option<String>,
156}
157
158pub fn initialize() {
159 INIT.call_once(|| embed::initialize(&OSDK_HOST, Vec::new()));
160}
161
162pub async fn install_packages(request: EmbeddedInstallRequest<'_>) -> Result<()> {
163 let registry = request.registry.clone();
164 let options = AddToProjectOptions {
165 save_exact: true,
166 ignore_scripts: !request.scripts_enabled,
167 dangerously_allow_all_builds: request.dangerously_allow_all_builds,
168 offline: request.offline,
169 dep_selection: DepSelection::All,
170 control: InstallControl::silent(),
171 runtime: Some(EmbedderRuntime::selector(request.node_bin_dir)),
172 ..Default::default()
173 };
174 let overrides = EmbedderInstallOverrides {
175 use_global_virtual_store: Some(false),
176 cache_dir: Some(request.cache_dir),
177 store_dir: Some(request.store_dir),
178 };
179
180 with_aube_registry_override(registry, || async {
181 initialize();
182 embed::add_with_overrides(request.project_dir, request.packages, options, overrides).await
183 })
184 .await
185 .map_err(|error| Error::other(format_aube_error(&error)))
186}
187
188pub async fn add_to_project(request: EmbeddedProjectAddRequest<'_>) -> Result<()> {
189 let registry = request.registry.clone();
190 let (options, overrides) = project_add_options(&request);
191
192 with_aube_registry_override(registry, || async {
193 initialize();
194 embed::add_with_overrides(request.project_dir, request.packages, options, overrides).await
195 })
196 .await
197 .map_err(|error| Error::other(format_aube_error(&error)))
198}
199
200fn project_add_options(
201 request: &EmbeddedProjectAddRequest<'_>,
202) -> (AddToProjectOptions, EmbedderInstallOverrides) {
203 let options = AddToProjectOptions {
204 save_dev: request.save_dev,
205 save_exact: false,
206 save_optional: request.save_optional,
207 save_peer: request.save_peer,
208 ignore_scripts: true,
209 dangerously_allow_all_builds: false,
210 offline: request.offline,
211 dep_selection: DepSelection::All,
212 control: InstallControl::silent(),
213 runtime: Some(EmbedderRuntime::selector(request.node_bin_dir.clone())),
214 ..Default::default()
215 };
216 let overrides = EmbedderInstallOverrides {
217 use_global_virtual_store: Some(false),
218 cache_dir: Some(request.cache_dir.clone()),
219 store_dir: Some(request.store_dir.clone()),
220 };
221 (options, overrides)
222}
223
224pub async fn install_frozen(request: EmbeddedFrozenInstallRequest<'_>) -> Result<()> {
225 let registry = request.registry.clone();
226 let (options, overrides) = frozen_install_options(request);
227 with_aube_registry_override(registry, || async {
228 initialize();
229 embed::install_with_overrides(options, overrides).await
230 })
231 .await
232 .map_err(|error| Error::other(format_aube_error(&error)))
233}
234
235pub async fn prepare_lock_graph(request: EmbeddedLockGraphRequest<'_>) -> Result<()> {
236 let registry = request.registry.clone();
237 let (options, overrides) = lock_graph_options(request);
238 with_aube_registry_override(registry, || async {
239 initialize();
240 embed::install_with_overrides(options, overrides).await
241 })
242 .await
243 .map_err(|error| Error::other(format_aube_error(&error)))
244}
245
246fn frozen_install_options(
247 request: EmbeddedFrozenInstallRequest<'_>,
248) -> (embed::InstallOptions, EmbedderInstallOverrides) {
249 let mut options = embed::InstallOptions::new(request.project_dir);
250 options.frozen_mode = FrozenMode::Frozen;
251 options.dep_selection = DepSelection::All;
252 options.ignore_scripts = !request.scripts_enabled;
253 options.run_root_lifecycle = request.scripts_enabled;
254 options.network_mode = if request.offline {
255 NetworkMode::Offline
256 } else {
257 NetworkMode::Online
258 };
259 options.strict_no_lockfile = true;
260 options.dangerously_allow_all_builds = request.dangerously_allow_all_builds;
261 options.control = InstallControl::silent();
262 options.runtime = Some(EmbedderRuntime::selector(request.node_bin_dir));
263
264 let overrides = EmbedderInstallOverrides {
265 use_global_virtual_store: Some(false),
266 cache_dir: Some(request.cache_dir),
267 store_dir: Some(request.store_dir),
268 };
269 (options, overrides)
270}
271
272fn lock_graph_options(
273 request: EmbeddedLockGraphRequest<'_>,
274) -> (embed::InstallOptions, EmbedderInstallOverrides) {
275 let mut options = embed::InstallOptions::new(request.project_dir);
276 options.frozen_mode = FrozenMode::Prefer;
277 options.dep_selection = DepSelection::All;
278 options.ignore_scripts = true;
279 options.run_root_lifecycle = false;
280 options.lockfile_only = true;
281 options.network_mode = if request.offline {
282 NetworkMode::Offline
283 } else {
284 NetworkMode::Online
285 };
286 options.control = InstallControl::silent();
287 options.runtime = Some(EmbedderRuntime::selector(request.node_bin_dir));
288
289 let overrides = EmbedderInstallOverrides {
290 use_global_virtual_store: Some(false),
291 cache_dir: Some(request.cache_dir),
292 store_dir: Some(request.store_dir),
293 };
294 (options, overrides)
295}
296
297fn format_aube_error(error: &impl std::fmt::Display) -> String {
298 error.to_string()
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::sync::Mutex as StdMutex;
305 use std::time::Duration;
306
307 use tokio::sync::{Barrier, Notify};
308
309 #[derive(Default)]
310 struct RecordingRegistryOverrideInstaller {
311 state: StdMutex<RecordingRegistryOverrideState>,
312 }
313
314 #[derive(Default)]
315 struct RecordingRegistryOverrideState {
316 current: Option<String>,
317 history: Vec<Option<String>>,
318 }
319
320 impl RegistryOverrideInstaller for RecordingRegistryOverrideInstaller {
321 fn install(&self, registry: Option<String>) {
322 let mut state = self.state.lock().expect("recording lock poisoned");
323 state.current = registry.clone();
324 state.history.push(registry);
325 }
326 }
327
328 impl RecordingRegistryOverrideInstaller {
329 fn current(&self) -> Option<String> {
330 self.state
331 .lock()
332 .expect("recording lock poisoned")
333 .current
334 .clone()
335 }
336
337 fn history(&self) -> Vec<Option<String>> {
338 self.state
339 .lock()
340 .expect("recording lock poisoned")
341 .history
342 .clone()
343 }
344 }
345
346 #[test]
347 fn embedded_host_uses_osdk_identity_and_disables_owned_behaviors() {
348 assert_eq!(OSDK_HOST.name, "osdk");
349 assert_eq!(OSDK_HOST.display_name, "osdk");
350 assert_eq!(OSDK_HOST.vendor, None);
351 assert!(!OSDK_HOST.runtime_switching);
352 assert!(!OSDK_HOST.self_engines_check);
353 assert!(!OSDK_HOST.self_update_enabled);
354 assert!(OSDK_HOST.canonical_lockfile_always_wins);
355 }
356
357 #[test]
358 fn frozen_install_uses_strict_graph_and_host_owned_runtime_and_storage() {
359 let project_dir = PathBuf::from("/tmp/osdk-aube-project");
360 let cache_dir = PathBuf::from("/tmp/osdk-aube-cache");
361 let store_dir = PathBuf::from("/tmp/osdk-aube-store");
362 let node_bin_dir = PathBuf::from("/tmp/osdk-node-bin");
363 let (options, overrides) = frozen_install_options(EmbeddedFrozenInstallRequest {
364 project_dir: &project_dir,
365 cache_dir: cache_dir.clone(),
366 store_dir: store_dir.clone(),
367 node_bin_dir,
368 scripts_enabled: false,
369 dangerously_allow_all_builds: false,
370 offline: true,
371 registry: None,
372 });
373
374 assert_eq!(options.project_dir, project_dir);
375 assert_eq!(options.frozen_mode, FrozenMode::Frozen);
376 assert_eq!(options.dep_selection, DepSelection::All);
377 assert!(options.ignore_scripts);
378 assert!(!options.run_root_lifecycle);
379 assert_eq!(options.network_mode, NetworkMode::Offline);
380 assert!(options.strict_no_lockfile);
381 assert!(!options.dangerously_allow_all_builds);
382 assert!(options.runtime.is_some());
383 assert_eq!(overrides.use_global_virtual_store, Some(false));
384 assert_eq!(overrides.cache_dir, Some(cache_dir));
385 assert_eq!(overrides.store_dir, Some(store_dir));
386 }
387
388 #[test]
389 fn lock_graph_generation_is_lockfile_only_and_uses_managed_runtime() {
390 let project_dir = PathBuf::from("/tmp/osdk-aube-project");
391 let cache_dir = PathBuf::from("/tmp/osdk-aube-cache");
392 let store_dir = PathBuf::from("/tmp/osdk-aube-store");
393 let (options, overrides) = lock_graph_options(EmbeddedLockGraphRequest {
394 project_dir: &project_dir,
395 cache_dir: cache_dir.clone(),
396 store_dir: store_dir.clone(),
397 node_bin_dir: PathBuf::from("/tmp/osdk-node-bin"),
398 offline: false,
399 registry: None,
400 });
401
402 assert_eq!(options.project_dir, project_dir);
403 assert_eq!(options.frozen_mode, FrozenMode::Prefer);
404 assert!(options.ignore_scripts);
405 assert!(!options.run_root_lifecycle);
406 assert!(options.lockfile_only);
407 assert_eq!(options.network_mode, NetworkMode::Online);
408 assert!(!options.strict_no_lockfile);
409 assert!(options.runtime.is_some());
410 assert_eq!(overrides.use_global_virtual_store, Some(false));
411 assert_eq!(overrides.cache_dir, Some(cache_dir));
412 assert_eq!(overrides.store_dir, Some(store_dir));
413 }
414
415 #[test]
416 fn project_add_options_disable_scripts_and_preserve_selected_section() {
417 let project_dir = PathBuf::from("/tmp/osdk-aube-project");
418 let cache_dir = PathBuf::from("/tmp/osdk-aube-cache");
419 let store_dir = PathBuf::from("/tmp/osdk-aube-store");
420 let request = EmbeddedProjectAddRequest {
421 project_dir: &project_dir,
422 packages: &["prettier@3".into()],
423 cache_dir: cache_dir.clone(),
424 store_dir: store_dir.clone(),
425 node_bin_dir: PathBuf::from("/tmp/osdk-node-bin"),
426 save_dev: true,
427 save_optional: false,
428 save_peer: false,
429 offline: false,
430 registry: None,
431 };
432 let (options, overrides) = project_add_options(&request);
433
434 assert!(options.save_dev);
435 assert!(!options.save_optional);
436 assert!(!options.save_peer);
437 assert!(options.ignore_scripts);
438 assert!(!options.dangerously_allow_all_builds);
439 assert!(options.runtime.is_some());
440 assert_eq!(overrides.use_global_virtual_store, Some(false));
441 assert_eq!(overrides.cache_dir, Some(cache_dir));
442 assert_eq!(overrides.store_dir, Some(store_dir));
443 }
444
445 #[tokio::test]
446 async fn registry_override_scope_passes_some_and_none_then_resets() {
447 let operation_lock = Mutex::new(());
448 let installer = RecordingRegistryOverrideInstaller::default();
449 let registry = "https://registry.example.test/".to_string();
450
451 let observed = with_registry_override(
452 &operation_lock,
453 &installer,
454 Some(registry.clone()),
455 || async { installer.current() },
456 )
457 .await;
458 assert_eq!(observed, Some(registry.clone()));
459 assert_eq!(installer.current(), None);
460
461 let observed = with_registry_override(&operation_lock, &installer, None, || async {
462 installer.current()
463 })
464 .await;
465 assert_eq!(observed, None);
466 assert_eq!(installer.current(), None);
467 assert_eq!(installer.history(), vec![Some(registry), None, None, None]);
468 }
469
470 #[tokio::test]
471 async fn registry_override_scope_resets_after_error() {
472 let operation_lock = Mutex::new(());
473 let installer = RecordingRegistryOverrideInstaller::default();
474 let registry = "https://broken.example.test/".to_string();
475
476 let result: std::result::Result<(), &'static str> = with_registry_override(
477 &operation_lock,
478 &installer,
479 Some(registry.clone()),
480 || async {
481 assert_eq!(installer.current(), Some(registry));
482 Err("expected failure")
483 },
484 )
485 .await;
486
487 assert_eq!(result, Err("expected failure"));
488 assert_eq!(installer.current(), None);
489 assert_eq!(
490 installer.history(),
491 vec![Some("https://broken.example.test/".to_string()), None]
492 );
493 }
494
495 #[tokio::test]
496 async fn registry_override_scope_resets_before_unlock_when_cancelled() {
497 let operation_lock = Mutex::new(());
498 let installer = RecordingRegistryOverrideInstaller::default();
499 let entered = Notify::new();
500
501 let mut operation = Box::pin(with_registry_override(
502 &operation_lock,
503 &installer,
504 Some("https://cancelled.example.test/".to_string()),
505 || async {
506 entered.notify_one();
507 std::future::pending::<()>().await;
508 },
509 ));
510
511 tokio::time::timeout(Duration::from_secs(5), async {
512 tokio::select! {
513 () = entered.notified() => {}
514 () = &mut operation => panic!("operation unexpectedly completed"),
515 }
516 })
517 .await
518 .expect("registry-scoped operation did not start");
519 drop(operation);
520
521 assert_eq!(installer.current(), None);
522 assert!(operation_lock.try_lock().is_ok());
523 assert_eq!(
524 installer.history(),
525 vec![Some("https://cancelled.example.test/".to_string()), None]
526 );
527 }
528
529 #[tokio::test]
530 async fn concurrent_registry_override_scopes_do_not_cross() {
531 let operation_lock = Mutex::new(());
532 let installer = RecordingRegistryOverrideInstaller::default();
533 let first_registry = "https://first.example.test/".to_string();
534 let second_registry = "https://second.example.test/".to_string();
535 let first_started = Barrier::new(3);
536 let release_first = Notify::new();
537
538 let first = with_registry_override(
539 &operation_lock,
540 &installer,
541 Some(first_registry.clone()),
542 || async {
543 assert_eq!(installer.current(), Some(first_registry.clone()));
544 first_started.wait().await;
545 release_first.notified().await;
546 assert_eq!(installer.current(), Some(first_registry.clone()));
547 },
548 );
549 let second = async {
550 first_started.wait().await;
551 with_registry_override(
552 &operation_lock,
553 &installer,
554 Some(second_registry.clone()),
555 || async {
556 assert_eq!(installer.current(), Some(second_registry.clone()));
557 },
558 )
559 .await;
560 };
561 let observe_while_second_is_waiting = async {
562 first_started.wait().await;
563 tokio::task::yield_now().await;
564 assert_eq!(installer.current(), Some(first_registry.clone()));
565 assert_eq!(installer.history(), vec![Some(first_registry.clone())]);
566 release_first.notify_one();
567 };
568
569 tokio::time::timeout(Duration::from_secs(5), async {
570 tokio::join!(first, second, observe_while_second_is_waiting);
571 })
572 .await
573 .expect("concurrent registry override scopes did not complete");
574
575 assert_eq!(installer.current(), None);
576 assert_eq!(
577 installer.history(),
578 vec![Some(first_registry), None, Some(second_registry), None]
579 );
580 }
581}