Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f281a9a
refactor(core): rename variable and launch specification types
cyberphantom52 Sep 10, 2026
9f5e579
refactor(core): move prefix storage under environment
cyberphantom52 Sep 10, 2026
509aa84
refactor(core): introduce private owner-cached environments
cyberphantom52 Sep 10, 2026
ce59a48
refactor(core): replace BottleEdit with owner edit callbacks
cyberphantom52 Sep 10, 2026
1e31c2e
fix(core): simplify live environments and independent shutdown
cyberphantom52 Sep 10, 2026
7a7edec
fix(core): centralize environment lifecycle
cyberphantom52 Sep 10, 2026
6b03147
fix(core): remove implicit FVS requirements from standard prefixes
cyberphantom52 Sep 10, 2026
f023ac3
feat(core): build independent addon layers against pinned Soda
cyberphantom52 Sep 11, 2026
9ffcebb
fix(core): simplify environment preparation and pin Virgo base
cyberphantom52 Sep 11, 2026
45c6dc4
refactor(core): derive Virgo composition from environment configuration
cyberphantom52 Sep 11, 2026
f05e718
refactor(core): derive addon environment variables on demand
cyberphantom52 Sep 11, 2026
408462c
refactor(core): materialize Virgo prefixes before execution
cyberphantom52 Sep 11, 2026
8bcf816
fix(core): build addon layers against Soda alone
cyberphantom52 Sep 11, 2026
22c2b82
refactor(core): centralize environment lifecycle workflows
cyberphantom52 Sep 11, 2026
f07cd1f
refactor(addons): persist immutable releases by UUID
cyberphantom52 Sep 11, 2026
fb89ae8
refactor(addons): persist runtime variables in selections
cyberphantom52 Sep 11, 2026
518db96
refactor(virgo): publish complete immutable addon artifacts
cyberphantom52 Sep 11, 2026
d7f5ba0
refactor(virgo): centralize shared artifact ownership
cyberphantom52 Sep 11, 2026
33cfa47
refactor(virgo): separate resolution from owner preparation
cyberphantom52 Sep 11, 2026
102f02e
refactor(virgo): bind shared manager to core services
cyberphantom52 Sep 11, 2026
21fb0fc
docs: trim README to overview and usage
cyberphantom52 Sep 11, 2026
86e4136
refactor(addons): separate runtime declarations from installer
cyberphantom52 Sep 11, 2026
c5c0b5b
fix(core): keep bottle opening registry-only
cyberphantom52 Sep 11, 2026
1b69ae1
refactor(history): delegate FVS stream completion
cyberphantom52 Sep 11, 2026
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
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

The application core for managing Bottles Next Wine and Proton environments.

`bottles-core` discovers and installs managed components, persists bottles,
executes Windows programs through WineBridge, and provides checkpointed prefix
mutation and snapshots through the default `fvs` feature.
`bottles-core` imports and installs managed components, persists bottles,
executes Windows programs through WineBridge, and provides Virgo storage and
snapshots through the default `fvs` feature. Standard creation, launch, and addon
changes work without FVS even when that feature is compiled in.

Disable FVS when only conventional, directly mutable prefixes are needed:

Expand All @@ -13,8 +14,9 @@ Disable FVS when only conventional, directly mutable prefixes are needed:
bottles-core = { version = "0.1", default-features = false }
```

Without `fvs`, snapshot APIs and Virgo storage are not compiled, and failed or
cancelled addon recipes are not rolled back automatically.
Without `fvs`, snapshot APIs and Virgo storage are not compiled. Standard addon
changes always use direct writes; failed or cancelled recipes can leave partial
prefix changes.

[Source] | [Issue tracker]

Expand All @@ -33,6 +35,22 @@ The crate is centered around six types:
- `Operation<T>` represents long-running work with progress and cooperative
cancellation.

Execution settings live in `BottleState::environment()` as an `EnvironmentConfig`.
Use `Bottle::edit` to update bottle state, and call `stop()` before changing
its environment settings. `Bottle::launch(ProgramSpec)` runs an unregistered
program; `Bottle::launch_program(uuid)` runs a saved registration. Dropping a
bottle handle leaves Wine running; call `stop()` to shut it down.

Choose a prefix backend when creating a bottle. Standard installs directly into
a conventional Wine prefix. Virgo is experimental: it combines shared immutable
layers with each bottle's private writable data, preparing missing layers at
startup. Virgo requires FVS and a downloaded Soda runner to build its base.

Fetch addons from the component and dependency catalogs, or use
`Addons::import_component` to import a component archive. Local releases are
identified by UUID. Removing a release deletes its installation inputs;
runtime executables and Standard recipe-based removal still require those files.

Operations are lazy. Await them, call `cancel().await`, or spawn them and
explicitly detach the task; dropping an operation abandons it.

Expand All @@ -50,7 +68,7 @@ futures-lite = "2"
Open the library, inspect the current bottles, and stop its download service:

```rust
use bottles_core::{Bottles, Config, Program, SearchSource};
use bottles_core::{Bottles, Config, ProgramSpec, SearchSource};
use futures_lite::StreamExt;

#[tokio::main]
Expand All @@ -65,11 +83,13 @@ async fn main() -> Result<(), bottles_core::error::Error> {
}

if let Some(bottle) = bottles.bottles().list().into_iter().next() {
let program = Program::new("Example", "C:/Games/example.exe")?;
let mut edit = bottle.edit();
edit.add_program(program.clone());
edit.commit().await?;
println!("registered {} as {}", program.name(), program.id());
let program = ProgramSpec::new("Example", "C:/Games/example.exe")?;
let id = program.id();
bottle.edit(move |state| {
state.programs.insert(program.id(), program);
Ok(())
}).await?;
println!("registered {id}");
}

let mut installed = Box::pin(bottles.library().watch());
Expand Down
39 changes: 26 additions & 13 deletions src/addons/addon.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Artifact-free addon selections and their family discriminators.
//! InstallResource-free addon selections and their family discriminators.

use std::{fmt, path::PathBuf, str::FromStr};

Expand All @@ -7,16 +7,16 @@ use strum::EnumIter;
use uuid::{NonNilUuid, Uuid};

use crate::{
Directories,
Directories, EnvVars,
error::Result,
runner::{Proton, Runner, RunnerError, RunnerKind, Wine, detect_runner_kind},
};

/// An addon selection persisted in a bottle.
///
/// `K` is [`Component`] or [`Dependency`]. Unlike an [`IndexEntry`](super::IndexEntry),
/// this value contains no download artifacts; it remains sufficient for requirement
/// validation and for locating or removing a selected component.
/// `K` is [`Component`] or [`Dependency`]. Unlike an [`Release`](super::Release),
/// this value contains no installation resources; it preserves requirements and
/// runtime variables independently of the shared release.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(
deny_unknown_fields,
Expand All @@ -28,6 +28,7 @@ pub struct Addon<K> {
version: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requirements: Vec<Requirement>,
env_vars: EnvVars,
#[serde(flatten)]
kind: K,
}
Expand All @@ -38,28 +39,30 @@ impl<K> Addon<K> {
name: String,
version: String,
requirements: Vec<Requirement>,
env_vars: EnvVars,
kind: K,
) -> Self {
Self {
id,
name,
version,
requirements,
env_vars,
kind,
}
}

/// Returns the release identifier shared by its catalog, index, and bottle records.
/// Returns the release identifier shared by its catalog, release, and bottle records.
pub fn id(&self) -> Uuid {
self.id.get()
}

/// Returns the catalog label, or version directory name for hand-placed components.
/// Returns the release label.
pub fn name(&self) -> &str {
&self.name
}

/// Returns the downloaded catalog or hand-placed version string.
/// Returns the release version string.
pub fn version(&self) -> &str {
&self.version
}
Expand All @@ -68,6 +71,16 @@ impl<K> Addon<K> {
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}

/// Returns this addon's frozen runtime environment variables.
///
/// Values are collected from the selected recipe's declarations during acquisition
/// and saved with the selection, so they remain available after the shared
/// release is removed. These are this addon's contributions only; environment
/// configuration combines them with other addons and applies owner overrides last.
pub fn env_vars(&self) -> &EnvVars {
&self.env_vars
}
}

impl Addon<Component> {
Expand All @@ -78,9 +91,9 @@ impl Addon<Component> {

pub(crate) fn path(&self, directories: &Directories) -> PathBuf {
directories
.components()
.join(self.slot().as_str())
.join(self.version())
.component_releases()
.join(self.id().to_string())
.join("payload")
}

/// Reports whether this component satisfies `requirement`.
Expand Down Expand Up @@ -208,14 +221,14 @@ pub enum Requirement {
Id(Uuid),
}

/// Type discriminator for component catalog, index, and bottle records.
/// Type discriminator for component catalog, release, and bottle records.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Component {
pub(crate) slot: Slot,
}

/// Type discriminator for dependency catalog, index, and bottle records.
/// Type discriminator for dependency catalog, release, and bottle records.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Dependency {}
47 changes: 20 additions & 27 deletions src/addons/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ enum Architecture {
/// One validated, cached catalog document.
///
/// Catalog loading is deliberately tolerant: an unavailable or invalid cache
/// is treated as absent so local index entries remain usable.
/// is treated as absent so local release records remain usable.
pub(crate) struct Catalog<K> {
#[serde(deserialize_with = "deserialize_catalog_version")]
schema_version: u32,
Expand Down Expand Up @@ -148,14 +148,14 @@ pub(crate) struct CatalogUrls {

/// Maps a family discriminator to its catalog URL and managed storage files.
///
/// Keeping this mapping on the two runtime families lets catalog and index
/// Keeping this mapping on the two runtime families lets catalog
/// persistence share generic code without introducing per-slot component types.
pub(crate) trait AddonFamily {
const LABEL: &'static str;

fn url(urls: &CatalogUrls) -> Option<Url>;
fn catalog(directories: &Directories) -> PathBuf;
fn index(directories: &Directories) -> PathBuf;
fn releases(directories: &Directories) -> PathBuf;
}

impl AddonFamily for Component {
Expand All @@ -169,8 +169,8 @@ impl AddonFamily for Component {
directories.components().join("catalog.json")
}

fn index(directories: &Directories) -> PathBuf {
directories.components().join("index.toml")
fn releases(directories: &Directories) -> PathBuf {
directories.component_releases()
}
}

Expand All @@ -185,8 +185,8 @@ impl AddonFamily for Dependency {
directories.dependencies().join("catalog.json")
}

fn index(directories: &Directories) -> PathBuf {
directories.dependencies().join("index.toml")
fn releases(directories: &Directories) -> PathBuf {
directories.dependency_releases()
}
}

Expand All @@ -203,8 +203,6 @@ pub struct CatalogEntry<K> {
name: String,
#[serde(deserialize_with = "deserialize_non_empty_string")]
version: String,
// Dependency requirements come from the catalog. Component requirements
// are derived from the downloaded release during inspection.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
requirements: Vec<Requirement>,
#[serde(deserialize_with = "deserialize_non_empty_vec")]
Expand All @@ -214,7 +212,7 @@ pub struct CatalogEntry<K> {
}

impl<K> CatalogEntry<K> {
/// Returns the identifier used to correlate this release with an index entry.
/// Returns the identifier used to correlate this release with a local release.
pub fn id(&self) -> Uuid {
self.id.get()
}
Expand All @@ -229,6 +227,11 @@ impl<K> CatalogEntry<K> {
&self.version
}

/// Requirements owned by this release definition, for either addon family.
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}

/// Reports whether at least one artifact matches the current build target.
///
/// Platform matching is exact. An artifact without a platform restriction
Expand All @@ -255,19 +258,12 @@ impl CatalogEntry<Component> {
}
}

impl CatalogEntry<Dependency> {
/// Returns the addons that must already be present before installation.
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
/// One downloadable file and the recipe associated with it.
///
/// Dependency recipes are retained in the local index. Components are inspected
/// after extraction and use the built-in recipe for their slot instead.
/// Both component and dependency recipes become part of the immutable release.
/// Components are extracted before their recipe is applied.
pub(crate) struct CatalogArtifact {
url: url::Url,
#[serde(deserialize_with = "deserialize_non_empty_string")]
Expand All @@ -276,25 +272,22 @@ pub(crate) struct CatalogArtifact {
checksum: Checksum,
#[serde(default, skip_serializing_if = "Option::is_none")]
platform: Option<Target>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
steps: Vec<InstallStep>,
#[serde(default, skip_serializing_if = "Option::is_none")]
steps: Option<Vec<InstallStep>>,
}

impl CatalogArtifact {
pub(crate) fn url(&self) -> &url::Url {
pub(crate) fn url(&self) -> &Url {
&self.url
}

pub(crate) fn file_name(&self) -> &str {
&self.file_name
}

pub(crate) fn checksum(&self) -> &Checksum {
&self.checksum
}

pub(crate) fn steps(&self) -> &[InstallStep] {
&self.steps
pub(crate) fn steps(&self) -> Option<&[InstallStep]> {
self.steps.as_deref()
}

fn matches(&self, target: Target) -> bool {
Expand Down
11 changes: 7 additions & 4 deletions src/addons/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub enum AddonError {
/// The requested release is unknown or not present on disk for removal.
#[error("addon {0} was not found")]
NotFound(Uuid),
/// A local release is incomplete or its payload was removed outside the manager.
#[error("source payload is missing for addon {0}")]
PayloadMissing(Uuid),
/// Local storage contains more than one addon with the same immutable identifier.
#[error("local storage contains duplicate addon {0}")]
Duplicate(Uuid),
Expand All @@ -37,10 +40,10 @@ pub enum AddonError {
/// A component directory lacks the marker required by its slot.
#[error("component could not be identified: {0}")]
InvalidComponent(PathBuf),
/// A local addon index contains inconsistent or unsafe metadata.
#[error("addon index is invalid: {0}")]
InvalidAddonIndex(PathBuf),
/// A component download would overwrite an existing version directory.
/// A release record contains inconsistent or unsafe metadata.
#[error("release record is invalid: {0}")]
InvalidRelease(PathBuf),
/// A component download would overwrite an existing release directory.
#[error("addon target already exists: {0}")]
TargetExists(PathBuf),
}
Expand Down
Loading