mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-12 05:03:07 +00:00
feat: gate bundle to OCI migration behind experimental feature
This commit is contained in:
6 files changed
+141
No files matched your search
@@ -345,3 +345,30 @@ Forks and modified versions must remove them.
|
||||
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>
|
||||
<sub>Made by the <a href="https://getartcraft.com/">ArtCraft</a> team and community.</sub>
|
||||
</p>
|
||||
|
||||
## Experimental OCI migration
|
||||
|
||||
The `experimental-oci-migration` Cargo feature is disabled by default and only acts on Linux.
|
||||
Enable it when building to test migrating existing first party Flatpak bundles to the
|
||||
ArtCraft OCI remote. For the launcher:
|
||||
|
||||
```sh
|
||||
cargo run -p artcraft-launcher --features experimental-oci-migration
|
||||
```
|
||||
|
||||
On an update, a first party installation with a bundle-created `-origin` remote may be
|
||||
reinstalled from the OCI repository in the same installation scope, using the same full
|
||||
Flatpak ref. Existing installations managed by another repository are not redirected.
|
||||
The migration reuses a remote with URL `oci+https://getartcraft.com`, or adds `artcraft-oci`
|
||||
using `https://getartcraft.com/artcraft-oci.flatpakrepo`. A conflicting remote name is an error.
|
||||
The current descriptor is a preview with `GPGVerify=false`; enabling this feature explicitly
|
||||
opts into that repository configuration. The launcher does not uninstall the app or remove
|
||||
its data. Subsequent updates use the detected OCI remote.
|
||||
|
||||
If the exact ref is absent, the existing verified bundle update proceeds. A repository error,
|
||||
cancellation, or missing/different repository version stops the update; migration requires
|
||||
an exact match to the selected release version. Launcher pinned/nightly tracks and explicit
|
||||
replacement operations do not trigger migration. The feature does not change new installs.
|
||||
|
||||
Test with an isolated Flatpak installation before enabling this in release builds. The
|
||||
feature is not enabled by the packaging defaults.
|
||||
@@ -13,6 +13,8 @@ publish = false
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# Opt-in migration of first party Flatpak bundles to the ArtCraft OCI remote.
|
||||
experimental-oci-migration = ["artcraft-launcher-native/experimental-oci-migration"]
|
||||
# Development only: install from `cargo xtask local serve` (see the README).
|
||||
local-mirror = ["artcraft-launcher-native/local-mirror"]
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ publish = false
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# Opt-in migration of first party Flatpak bundles to the ArtCraft OCI remote.
|
||||
experimental-oci-migration = []
|
||||
# Lets ARTCRAFT_LAUNCHER_DOWNLOAD_BASE point the launcher at a developer's own loopback mirror
|
||||
# (`cargo xtask local serve`). Development only: release builds never enable it, so a shipped
|
||||
# launcher can't be steered away from GitHub's releases by an environment variable.
|
||||
|
||||
@@ -566,6 +566,16 @@ fn run_install(
|
||||
if !release_matches_track(&release, track, options.prereleases) {
|
||||
return Err("the selected version isn't available yet. Check for updates before installing.".into());
|
||||
}
|
||||
#[cfg(all(target_os = "linux", feature = "experimental-oci-migration"))]
|
||||
if matches!(track, VersionTrack::Stable)
|
||||
&& !release.prerelease
|
||||
&& !replace
|
||||
&& let Some(installed) = existing
|
||||
&& let Some(expected) = release.version.as_ref()
|
||||
&& install::linux::migrate_bundle_to_oci(app, installed, expected, stop)?
|
||||
{
|
||||
return Ok(format!("{} now updates through the ArtCraft OCI repository", app.name));
|
||||
}
|
||||
let (asset, kind) = planned_asset(app, &release, platform, options, existing, record).ok_or_else(|| {
|
||||
if platform.os == artcraft_launcher_core::Os::Linux
|
||||
&& options.install_locations.folder(app.id).is_some()
|
||||
|
||||
@@ -1226,6 +1226,74 @@ pub(crate) mod linux {
|
||||
}
|
||||
}
|
||||
|
||||
/// Experimental in-place migration; only an absent ref permits bundle fallback.
|
||||
#[cfg(feature = "experimental-oci-migration")]
|
||||
pub fn migrate_bundle_to_oci(app: &CraftApp, installed: &Installed, expected: &super::version::Version, stop: &Stop) -> Result<bool, String> {
|
||||
if installed.source != InstallSource::Flatpak || installed.externally_managed || !app.repo.starts_with("storytold/") {
|
||||
return Ok(false);
|
||||
}
|
||||
// Only bundle-created origins qualify; never redirect another repository's install.
|
||||
if !installed.origin.as_deref().is_some_and(|origin| origin.ends_with("-origin")) {
|
||||
return Ok(false);
|
||||
}
|
||||
let id = installed.package.as_deref().ok_or("Flatpak installation has no package ID")?;
|
||||
let scope = match installed.installation.as_deref() {
|
||||
Some("user") => "--user".to_string(),
|
||||
Some("system") => "--system".to_string(),
|
||||
Some(name) => format!("--installation={name}"),
|
||||
None => return Err("Flatpak installation scope is unknown".into()),
|
||||
};
|
||||
let run = |args: &[&str]| -> Result<String, String> {
|
||||
let mut cmd = if process::in_sandbox() {
|
||||
let mut cmd = process::command("flatpak-spawn");
|
||||
cmd.args(["--host", "--watch-bus", "flatpak"]);
|
||||
cmd
|
||||
} else {
|
||||
process::command("flatpak")
|
||||
};
|
||||
cmd.arg(&scope).args(args);
|
||||
let output = process::cancellable_output(cmd, || stop.check())?;
|
||||
if !output.status.success() {
|
||||
return Err(format!("Flatpak OCI migration failed: {}", String::from_utf8_lossy(&output.stderr).trim()));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
};
|
||||
let reference = run(&["info", "--show-ref", id])?;
|
||||
let reference = reference.trim();
|
||||
if !reference.starts_with(&format!("app/{id}/")) || !reference.ends_with("/stable") {
|
||||
return Ok(false);
|
||||
}
|
||||
let remotes = run(&["remotes", "--columns=name,url"])?;
|
||||
let remote = remotes.lines().find_map(|line| {
|
||||
let (name, _) = line.split_once('\t')?;
|
||||
crate::packages::is_artcraft_remote(name, line).then(|| name.to_string())
|
||||
});
|
||||
let remote = match remote {
|
||||
Some(remote) => remote,
|
||||
None => {
|
||||
if remotes.lines().any(|line| line.split('\t').next() == Some("artcraft-oci")) {
|
||||
return Err("The artcraft-oci remote points to a different repository".into());
|
||||
}
|
||||
run(&["remote-add", "--noninteractive", "artcraft-oci", "https://getartcraft.com/artcraft-oci.flatpakrepo"])?;
|
||||
let remotes = run(&["remotes", "--columns=name,url"])?;
|
||||
if !crate::packages::is_artcraft_remote("artcraft-oci", &remotes) {
|
||||
return Err("The ArtCraft OCI descriptor configured an unexpected repository".into());
|
||||
}
|
||||
"artcraft-oci".to_string()
|
||||
}
|
||||
};
|
||||
let refs = run(&["remote-ls", "--app", "--columns=ref,version", &remote])?;
|
||||
if !crate::packages::oci_ref_available(reference, expected, &refs)? {
|
||||
return Ok(false);
|
||||
}
|
||||
run(&["install", "--reinstall", "-y", "--noninteractive", &remote, reference])?;
|
||||
let origin = run(&["info", "--show-origin", id])?;
|
||||
if origin.trim() != remote {
|
||||
return Err("Flatpak did not switch the installed app to the ArtCraft OCI remote".into());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Updates an installed Flatpak from its remote repository (such as FlatPark).
|
||||
pub fn update_flatpak_app(
|
||||
app: &CraftApp,
|
||||
|
||||
@@ -273,3 +273,35 @@ mod remote_url_tests {
|
||||
assert!(!is_artcraft_remote("other", "custom\toci+https://getartcraft.com"));
|
||||
}
|
||||
}
|
||||
|
||||
/// A missing ref permits bundle fallback; an unknown or different version must not replace it.
|
||||
#[cfg(all(target_os = "linux", feature = "experimental-oci-migration"))]
|
||||
pub(crate) fn oci_ref_available(reference: &str, expected: &version::Version, listing: &str) -> Result<bool, String> {
|
||||
for line in listing.lines() {
|
||||
let mut fields = line.split('\t');
|
||||
if fields.next().map(str::trim) != Some(reference) {
|
||||
continue;
|
||||
}
|
||||
let candidate = fields.next().and_then(|value| version::parse(value.trim()));
|
||||
if candidate.as_ref() != Some(expected) {
|
||||
return Err("The ArtCraft OCI repository does not yet provide the selected version".into());
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "linux", feature = "experimental-oci-migration"))]
|
||||
mod migration_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn migration_requires_the_exact_ref_and_selected_version() {
|
||||
let reference = "app/ai.storyteller.photocraft/x86_64/stable";
|
||||
let expected = version::parse("1.2.3").unwrap();
|
||||
assert_eq!(oci_ref_available(reference, &expected, &format!("{reference}\t1.2.3")), Ok(true));
|
||||
assert_eq!(oci_ref_available(reference, &expected, "app/ai.storyteller.photocraft/aarch64/stable\t1.2.3"), Ok(false));
|
||||
for value in ["", "1.2.2", "1.2.4", "unknown"] {
|
||||
assert!(oci_ref_available(reference, &expected, &format!("{reference}\t{value}")).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user