Skip to main content

osdk_core/backend/
aube_host.rs

1//! osdk's narrowly-scoped host adapter for the embedded aube package engine.
2
3use 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    // Keep aube's private synthetic-project format. These names never leak
22    // into the user's project because osdk owns the whole project directory.
23    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();
39// Aube 2.1 stores its CLI registry override in a process-global RwLock. Keep
40// every embedded call in one critical section so each operation observes only
41// its own override. The reset guard is declared after the mutex guard below,
42// ensuring cancellation clears the override before another caller acquires it.
43static 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
122/// Add one or more dependencies directly to a user-owned project. Project
123/// installs deliberately share osdk's Aube cache and store, but always use the
124/// caller-selected managed Node runtime and never execute lifecycle scripts.
125pub 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}