Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions devenv-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ pub struct Config {
#[serde(skip_serializing_if = "Option::is_none", default)]
#[setting(merge = schematic::merge::replace)]
pub profile: Option<String>,
#[setting(nested)]
#[serde(skip_serializing_if = "Option::is_none", default)]
pub sandbox: Option<SandboxConfig>,
/// Git repository root path (not serialized, computed during load)
#[serde(skip)]
pub git_root: Option<PathBuf>,
Expand All @@ -201,6 +204,90 @@ pub struct SecretspecConfig {
pub provider: Option<String>,
}

#[derive(schematic::Config, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[config(rename_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub struct SandboxConfig {
#[serde(skip_serializing_if = "is_false", default = "false_default")]
#[setting(default = false)]
pub enable: bool,

/// Network configuration for the sandbox
#[setting(nested)]
#[serde(skip_serializing_if = "Option::is_none", default)]
pub network: Option<SandboxNetwork>,

/// Mount resources into sandbox
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[setting(nested, merge = schematic::merge::append_vec)]
pub mounts: Vec<SandboxBindMount>,
}

#[derive(schematic::Config, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SandboxNetwork {
#[serde(skip_serializing_if = "is_true", default = "true_default")]
#[setting(default = true)]
pub enable: bool,

#[serde(skip_serializing_if = "is_true", default = "true_default")]
#[setting(default = true)]
pub host_resolv: bool,

#[serde(skip_serializing_if = "is_true", default = "true_default")]
#[setting(default = true)]
pub host_certs: bool,
}

#[derive(schematic::Config, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[config(rename_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub struct SandboxBindMount {
/// Source path on the host
pub path: String,

/// Destination path in the sandbox (defaults to same as source)
#[serde(skip_serializing_if = "Option::is_none", default)]
pub dest: Option<String>,

/// Mount mode (readonly, readwrite, device, tmpfs)
#[serde(skip_serializing_if = "Option::is_none", default)]
pub mode: Option<BindMountMode>,

/// Skip non-existent resources
#[serde(skip_serializing_if = "is_false", default = "false_default")]
#[setting(default = false)]
pub optional: bool,

/// Late binding, executed after workspace already mounted into sandbox
#[serde(skip_serializing_if = "is_false", default = "false_default")]
#[setting(default = false)]
pub late: bool,

#[serde(skip_serializing_if = "is_false", default = "false_default")]
#[setting(default = false)]
pub canonicalize: bool,
}

#[derive(
schematic::ConfigEnum, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum BindMountMode {
#[default]
#[serde(alias = "ro")]
ReadOnly,
#[serde(alias = "rw")]
ReadWrite,
#[serde(alias = "dev")]
Device,
Tmpfs,
Overlay,
OverlayTmpfs,
OverlayRo,
// persistent private state directory
StateDir,
}

// TODO: https://github.com/moonrepo/schematic/issues/105
pub async fn write_json_schema() -> Result<()> {
let schema = schema_for!(Config);
Expand Down Expand Up @@ -340,6 +427,43 @@ impl Config {
})?;
}

// Shared ~/.config/devenv/devenv.yaml
if let Ok(home) = std::env::var("HOME") {
if let Ok(home_path) = std::fs::canonicalize(home) {
let shared_config_dir = home_path.join(".config").join("devenv");
let config_yaml = shared_config_dir.join("devenv.yaml");
if config_yaml.exists() {
let mut shared_loader = ConfigLoader::<Config>::new();
shared_loader
.file_optional(&config_yaml)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"Failed to load shared configuration file: {}",
config_yaml.display()
)
})?;
if let Ok(local_result) = shared_loader.load().into_diagnostic() {
for input_name in local_result.config.inputs.keys() {
input_source_dirs
.entry(input_name.clone())
.or_insert_with(|| shared_config_dir.to_path_buf());
}
}

loader
.file_optional(&config_yaml)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"Failed to load shared configuration file: {}",
config_yaml.display()
)
})?;
}
}
}

// Load devenv.local.yaml last (if it exists) to allow local overrides
let local_yaml = base_path.join(YAML_LOCAL_CONFIG);
if local_yaml.exists() {
Expand Down
8 changes: 8 additions & 0 deletions devenv-core/src/nix_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ pub trait NixBackend: Send + Sync {
/// Get the bash shell executable path
async fn get_bash(&self, refresh_cached_output: bool) -> Result<String>;

/// Get package executable path
async fn get_executable(
&self,
package_name: &str,
name: &str,
refresh_cached_output: bool,
) -> Result<String>;

/// Check if the current user is a trusted user of the Nix store
async fn is_trusted_user(&self) -> Result<bool>;

Expand Down
39 changes: 39 additions & 0 deletions devenv-nix-backend/src/nix_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1884,6 +1884,45 @@ impl NixBackend for NixRustBackend {
))
}

async fn get_executable(
&self,
package: &str,
name: &str,
refresh_cached_output: bool,
) -> Result<String> {
// Get package executable path for this system
let gc_root_base = self.paths.dotfile.join(package);
let gc_root_actual = self.paths.dotfile.join(format!("{package}-{name}"));

// Try cache first
if !refresh_cached_output
&& gc_root_actual.exists()
&& let Ok(cached_path) = std::fs::read_link(&gc_root_actual)
{
// Verify the path still exists in the store
if cached_path.exists() {
let path_str = cached_path.to_string_lossy().to_string();
return Ok(format!("{path_str}/bin/{name}"));
}
}

// Cache miss or refresh requested - use build() which handles everything
let paths = self
.build(&[package], None, Some(&gc_root_base))
.await
.wrap_err_with(|| format!("Failed to build {package} attribute from default.nix"))?;

if paths.is_empty() {
return Err(miette!("No output paths from {package} build"));
}

// Return the path to the bash executable
Ok(format!(
"{store_path}/bin/{name}",
store_path = paths[0].to_string_lossy()
))
}

async fn is_trusted_user(&self) -> Result<bool> {
// Check if the current user is trusted by the Nix daemon/store
// This is used to determine if we can safely add substituters
Expand Down
10 changes: 10 additions & 0 deletions devenv-snix-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ impl NixBackend for SnixBackend {
bail!("get_bash is not yet implemented for Snix backend")
}

async fn get_executable(
&self,
_package: &str,
_name: &str,
_refresh_cached_output: bool,
) -> Result<String> {
// TODO: Implement bash shell acquisition for Snix backend
bail!("get_executable is not yet implemented for Snix backend")
}

async fn is_trusted_user(&self) -> Result<bool> {
// TODO: Implement trusted user check for Snix backend
bail!("is_trusted_user is not yet implemented for Snix backend")
Expand Down
Loading