| 534 |
534 |
|
backends.push(std::boxed::Box::new(Podman));
|
| 535 |
535 |
|
}
|
| 536 |
536 |
|
if Invocation::new("flatpak").arg("--version").probe() {
|
| 537 |
|
- |
backends.push(std::boxed::Box::new(Flatpak));
|
|
537 |
+ |
backends.push(std::boxed::Box::new(Flatpak::new()));
|
| 538 |
538 |
|
}
|
| 539 |
539 |
|
backends
|
| 540 |
540 |
|
}
|
| 923 |
923 |
|
|
| 924 |
924 |
|
// ---- flatpak: the sandboxed level ----
|
| 925 |
925 |
|
|
| 926 |
|
- |
pub(crate) struct Flatpak;
|
|
926 |
+ |
pub(crate) struct Flatpak {
|
|
927 |
+ |
/// How the backend asks the machine which remotes are configured.
|
|
928 |
+ |
///
|
|
929 |
+ |
/// A seam rather than a direct call because it is the one place a backend
|
|
930 |
+ |
/// reads the machine outside `list`, and the branch it feeds — the message
|
|
931 |
+ |
/// a first install on a fresh machine gets — has to be testable without a
|
|
932 |
+ |
/// flatpak installation to arrange. `None` means the question could not be
|
|
933 |
+ |
/// asked, which never blocks an install: a probe that failed is not
|
|
934 |
+ |
/// evidence that there is nowhere to install from.
|
|
935 |
+ |
remotes: fn() -> Option<Vec<String>>,
|
|
936 |
+ |
}
|
|
937 |
+ |
|
|
938 |
+ |
impl Flatpak {
|
|
939 |
+ |
pub(crate) fn new() -> Self {
|
|
940 |
+ |
Self {
|
|
941 |
+ |
remotes: configured_remotes,
|
|
942 |
+ |
}
|
|
943 |
+ |
}
|
|
944 |
+ |
}
|
|
945 |
+ |
|
|
946 |
+ |
/// The remotes flatpak would resolve an app against, or `None` if it could not
|
|
947 |
+ |
/// be asked.
|
|
948 |
+ |
fn configured_remotes() -> Option<Vec<String>> {
|
|
949 |
+ |
let raw = Invocation::new("flatpak")
|
|
950 |
+ |
.args(["remotes", "--columns=name"])
|
|
951 |
+ |
.capture_quiet()
|
|
952 |
+ |
.ok()?;
|
|
953 |
+ |
Some(parse_remotes(&raw))
|
|
954 |
+ |
}
|
|
955 |
+ |
|
|
956 |
+ |
/// One remote per non-empty line, which is what `--columns=name` prints.
|
|
957 |
+ |
fn parse_remotes(raw: &str) -> Vec<String> {
|
|
958 |
+ |
raw.lines()
|
|
959 |
+ |
.map(str::trim)
|
|
960 |
+ |
.filter(|line| !line.is_empty())
|
|
961 |
+ |
.map(ToString::to_string)
|
|
962 |
+ |
.collect()
|
|
963 |
+ |
}
|
|
964 |
+ |
|
|
965 |
+ |
/// What a sandboxed install says on a machine that has no remote configured.
|
|
966 |
+ |
///
|
|
967 |
+ |
/// Alloy ships the flatpak client and no catalog: choosing a catalog is
|
|
968 |
+ |
/// choosing who the user's software comes from, and that is the user's call
|
|
969 |
+ |
/// (wiki `alloy-byo-principle`). So this is a decision the console has to own
|
|
970 |
+ |
/// out loud — flatpak's own "no remote refs found" reads as the app being
|
|
971 |
+ |
/// missing rather than as there being nowhere to look.
|
|
972 |
+ |
fn no_remote_message(app: &str) -> String {
|
|
973 |
+ |
format!(
|
|
974 |
+ |
"no flatpak remote is configured, so there is nowhere to install {app} from. \
|
|
975 |
+ |
Alloy ships the flatpak client and no catalog, because which catalog you \
|
|
976 |
+ |
install from is your choice, not the image's. Add one with \
|
|
977 |
+ |
`flatpak remote-add --user --if-not-exists <name> <url>`, or name a remote \
|
|
978 |
+ |
in the box's spec entry"
|
|
979 |
+ |
)
|
|
980 |
+ |
}
|
| 927 |
981 |
|
|
| 928 |
982 |
|
/// Columns requested from `flatpak list`, in the order the parser reads them.
|
| 929 |
983 |
|
///
|
| 1012 |
1066 |
|
let app = spec.app(name)?;
|
| 1013 |
1067 |
|
let mut invocation =
|
| 1014 |
1068 |
|
Invocation::new("flatpak").args(["install", Scope::User.flag(), "--noninteractive"]);
|
| 1015 |
|
- |
if let Some(remote) = &spec.remote {
|
| 1016 |
|
- |
invocation = invocation.arg(remote);
|
|
1069 |
+ |
match &spec.remote {
|
|
1070 |
+ |
Some(remote) => invocation = invocation.arg(remote),
|
|
1071 |
+ |
// Only when the spec names none: a box that declares its remote
|
|
1072 |
+ |
// says where it comes from, and flatpak's own error is the right
|
|
1073 |
+ |
// one if that remote is not configured.
|
|
1074 |
+ |
None => {
|
|
1075 |
+ |
if (self.remotes)().is_some_and(|remotes| remotes.is_empty()) {
|
|
1076 |
+ |
anyhow::bail!(no_remote_message(app));
|
|
1077 |
+ |
}
|
|
1078 |
+ |
}
|
| 1017 |
1079 |
|
}
|
| 1018 |
1080 |
|
Ok(invocation.arg(app))
|
| 1019 |
1081 |
|
}
|
| 2490 |
2552 |
|
// here even though it never does for podman.
|
| 2491 |
2553 |
|
#[test]
|
| 2492 |
2554 |
|
fn every_flatpak_app_is_sandboxed_even_when_ad_hoc() {
|
| 2493 |
|
- |
let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
|
|
2555 |
+ |
let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
|
| 2494 |
2556 |
|
assert_eq!(boxes.len(), 2);
|
| 2495 |
2557 |
|
for boxed in &boxes {
|
| 2496 |
2558 |
|
assert_eq!(boxed.level, Some(Level::Sandboxed));
|
| 2503 |
2565 |
|
// is what identifies it to flatpak.
|
| 2504 |
2566 |
|
#[test]
|
| 2505 |
2567 |
|
fn a_sandboxed_row_shows_the_name_and_carries_the_app_id() {
|
| 2506 |
|
- |
let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
|
|
2568 |
+ |
let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
|
| 2507 |
2569 |
|
let inkscape = boxes
|
| 2508 |
2570 |
|
.iter()
|
| 2509 |
2571 |
|
.find(|b| b.source == "org.inkscape.Inkscape")
|
| 2526 |
2588 |
|
)
|
| 2527 |
2589 |
|
.unwrap();
|
| 2528 |
2590 |
|
|
| 2529 |
|
- |
let boxes = Flatpak.parse(FLATPAK, &spec).unwrap();
|
|
2591 |
+ |
let boxes = flatpak().parse(FLATPAK, &spec).unwrap();
|
| 2530 |
2592 |
|
let inkscape = boxes
|
| 2531 |
2593 |
|
.iter()
|
| 2532 |
2594 |
|
.find(|b| b.source == "org.inkscape.Inkscape")
|
| 2548 |
2610 |
|
// An app with no human name still needs an identifiable row.
|
| 2549 |
2611 |
|
#[test]
|
| 2550 |
2612 |
|
fn a_flatpak_row_with_a_missing_name_falls_back_to_the_app_id() {
|
| 2551 |
|
- |
let boxes = Flatpak
|
|
2613 |
+ |
let boxes = flatpak()
|
| 2552 |
2614 |
|
.parse("org.example.Thing\t\tflathub\tuser\n", &Spec::default())
|
| 2553 |
2615 |
|
.unwrap();
|
| 2554 |
2616 |
|
assert_eq!(boxes[0].name, "org.example.Thing");
|
| 2558 |
2620 |
|
// the two rows of the fixture must not come out of the parser alike.
|
| 2559 |
2621 |
|
#[test]
|
| 2560 |
2622 |
|
fn a_sandboxed_row_carries_the_installation_it_was_found_in() {
|
| 2561 |
|
- |
let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
|
|
2623 |
+ |
let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
|
| 2562 |
2624 |
|
let inkscape = boxes
|
| 2563 |
2625 |
|
.iter()
|
| 2564 |
2626 |
|
.find(|b| b.source == "org.inkscape.Inkscape")
|
| 2570 |
2632 |
|
assert_eq!(inkscape.scope, Some(Scope::User));
|
| 2571 |
2633 |
|
assert_eq!(tasks.scope, Some(Scope::System));
|
| 2572 |
2634 |
|
assert_eq!(
|
| 2573 |
|
- |
Flatpak.remove(tasks).display(),
|
|
2635 |
+ |
flatpak().remove(tasks).display(),
|
| 2574 |
2636 |
|
"flatpak uninstall --system dev.edfloreshz.Tasks",
|
| 2575 |
2637 |
|
"a system app is removed from the system installation, not the user one"
|
| 2576 |
2638 |
|
);
|
| 2581 |
2643 |
|
// way it did before the column was read, rather than guessing at `--user`.
|
| 2582 |
2644 |
|
#[test]
|
| 2583 |
2645 |
|
fn an_unrecognized_installation_leaves_removal_unqualified() {
|
| 2584 |
|
- |
let boxes = Flatpak
|
|
2646 |
+ |
let boxes = flatpak()
|
| 2585 |
2647 |
|
.parse(
|
| 2586 |
2648 |
|
"org.example.Thing\tThing\tflathub\tmy-ssd\n",
|
| 2587 |
2649 |
|
&Spec::default(),
|
| 2589 |
2651 |
|
.unwrap();
|
| 2590 |
2652 |
|
assert_eq!(boxes[0].scope, None);
|
| 2591 |
2653 |
|
assert_eq!(
|
| 2592 |
|
- |
Flatpak.remove(&boxes[0]).display(),
|
|
2654 |
+ |
flatpak().remove(&boxes[0]).display(),
|
| 2593 |
2655 |
|
"flatpak uninstall org.example.Thing"
|
| 2594 |
2656 |
|
);
|
| 2595 |
2657 |
|
}
|
| 2605 |
2667 |
|
let without =
|
| 2606 |
2668 |
|
Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.T\"\n").unwrap();
|
| 2607 |
2669 |
|
for argv in [
|
| 2608 |
|
- |
create_argv(&Flatpak, &with_remote, "inkscape"),
|
| 2609 |
|
- |
create_argv(&Flatpak, &without, "thing"),
|
|
2670 |
+ |
create_argv(&flatpak(), &with_remote, "inkscape"),
|
|
2671 |
+ |
create_argv(&flatpak(), &without, "thing"),
|
| 2610 |
2672 |
|
] {
|
| 2611 |
2673 |
|
assert!(argv.contains(" --user "), "got: {argv}");
|
| 2612 |
2674 |
|
assert!(!argv.contains("--system"), "got: {argv}");
|
| 2617 |
2679 |
|
// not read as a parse failure.
|
| 2618 |
2680 |
|
#[test]
|
| 2619 |
2681 |
|
fn empty_flatpak_output_parses_to_an_empty_list() {
|
| 2620 |
|
- |
assert!(Flatpak.parse("", &Spec::default()).unwrap().is_empty());
|
| 2621 |
|
- |
assert!(Flatpak.parse("\n\n", &Spec::default()).unwrap().is_empty());
|
|
2682 |
+ |
assert!(flatpak().parse("", &Spec::default()).unwrap().is_empty());
|
|
2683 |
+ |
assert!(
|
|
2684 |
+ |
flatpak()
|
|
2685 |
+ |
.parse("\n\n", &Spec::default())
|
|
2686 |
+ |
.unwrap()
|
|
2687 |
+ |
.is_empty()
|
|
2688 |
+ |
);
|
| 2622 |
2689 |
|
}
|
| 2623 |
2690 |
|
|
| 2624 |
2691 |
|
// ---- commands ----
|
| 2692 |
2759 |
|
|
| 2693 |
2760 |
|
#[test]
|
| 2694 |
2761 |
|
fn flatpak_commands_address_the_app_id() {
|
| 2695 |
|
- |
let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
|
|
2762 |
+ |
let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
|
| 2696 |
2763 |
|
let inkscape = boxes
|
| 2697 |
2764 |
|
.iter()
|
| 2698 |
2765 |
|
.find(|b| b.source == "org.inkscape.Inkscape")
|
| 2699 |
2766 |
|
.unwrap();
|
| 2700 |
2767 |
|
|
| 2701 |
2768 |
|
assert_eq!(
|
| 2702 |
|
- |
Flatpak.list().display(),
|
|
2769 |
+ |
flatpak().list().display(),
|
| 2703 |
2770 |
|
"flatpak list --app --columns=application,name,origin,installation"
|
| 2704 |
2771 |
|
);
|
| 2705 |
2772 |
|
assert_eq!(
|
| 2706 |
|
- |
Flatpak.remove(inkscape).display(),
|
|
2773 |
+ |
flatpak().remove(inkscape).display(),
|
| 2707 |
2774 |
|
"flatpak uninstall --user org.inkscape.Inkscape"
|
| 2708 |
2775 |
|
);
|
| 2709 |
2776 |
|
assert_eq!(
|
| 2710 |
|
- |
Flatpak.enter(inkscape).display(),
|
|
2777 |
+ |
flatpak().enter(inkscape).display(),
|
| 2711 |
2778 |
|
"flatpak run --command=sh org.inkscape.Inkscape"
|
| 2712 |
2779 |
|
);
|
| 2713 |
|
- |
assert!(Flatpak.start(inkscape).is_none());
|
| 2714 |
|
- |
assert!(Flatpak.stop(inkscape).is_none());
|
|
2780 |
+ |
assert!(flatpak().start(inkscape).is_none());
|
|
2781 |
+ |
assert!(flatpak().stop(inkscape).is_none());
|
| 2715 |
2782 |
|
}
|
| 2716 |
2783 |
|
|
| 2717 |
2784 |
|
// ---- exporting ----
|
| 2875 |
2942 |
|
#[test]
|
| 2876 |
2943 |
|
fn flatpak_refuses_to_export_and_says_the_app_is_already_there() {
|
| 2877 |
2944 |
|
let spec = export_spec();
|
| 2878 |
|
- |
let err = exports(&Flatpak, &spec, "inkscape")
|
|
2945 |
+ |
let err = exports(&flatpak(), &spec, "inkscape")
|
| 2879 |
2946 |
|
.unwrap_err()
|
| 2880 |
2947 |
|
.to_string();
|
| 2881 |
2948 |
|
assert!(err.contains("already exports"), "{err}");
|
| 2921 |
2988 |
|
.unwrap()
|
| 2922 |
2989 |
|
}
|
| 2923 |
2990 |
|
|
|
2991 |
+ |
/// A flatpak backend on an ordinary machine: one remote configured.
|
|
2992 |
+ |
///
|
|
2993 |
+ |
/// Every test that is not about the remote probe wants this, since a
|
|
2994 |
+ |
/// machine with a catalog is what a user who added one has.
|
|
2995 |
+ |
fn flatpak() -> Flatpak {
|
|
2996 |
+ |
Flatpak {
|
|
2997 |
+ |
remotes: || Some(vec!["example".to_string()]),
|
|
2998 |
+ |
}
|
|
2999 |
+ |
}
|
|
3000 |
+ |
|
| 2924 |
3001 |
|
fn create_argv(backend: &dyn Backend, spec: &Spec, name: &str) -> String {
|
| 2925 |
3002 |
|
let (_, entry) = spec.resolve(name).unwrap();
|
| 2926 |
3003 |
|
backend.create(name, entry).unwrap().display()
|
| 2957 |
3034 |
|
fn a_sandboxed_box_is_installed_from_its_declared_remote() {
|
| 2958 |
3035 |
|
let spec = creation_spec();
|
| 2959 |
3036 |
|
assert_eq!(
|
| 2960 |
|
- |
create_argv(&Flatpak, &spec, "inkscape"),
|
|
3037 |
+ |
create_argv(&flatpak(), &spec, "inkscape"),
|
| 2961 |
3038 |
|
"flatpak install --user --noninteractive flathub org.inkscape.Inkscape"
|
| 2962 |
3039 |
|
);
|
| 2963 |
3040 |
|
}
|
| 2968 |
3045 |
|
let spec = Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.Thing\"\n")
|
| 2969 |
3046 |
|
.unwrap();
|
| 2970 |
3047 |
|
assert_eq!(
|
| 2971 |
|
- |
create_argv(&Flatpak, &spec, "thing"),
|
|
3048 |
+ |
create_argv(&flatpak(), &spec, "thing"),
|
| 2972 |
3049 |
|
"flatpak install --user --noninteractive org.example.Thing"
|
| 2973 |
3050 |
|
);
|
| 2974 |
3051 |
|
}
|
| 2975 |
3052 |
|
|
|
3053 |
+ |
// Alloy ships the flatpak client and no catalog, so the first sandboxed
|
|
3054 |
+ |
// install on a fresh machine has nowhere to resolve from. Flatpak's own
|
|
3055 |
+ |
// error for that reads as the app being missing, which sends a user
|
|
3056 |
+ |
// looking for a typo in an app id that is correct.
|
|
3057 |
+ |
#[test]
|
|
3058 |
+ |
fn a_first_install_with_no_remote_configured_says_alloy_ships_none() {
|
|
3059 |
+ |
let spec = Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.Thing\"\n")
|
|
3060 |
+ |
.unwrap();
|
|
3061 |
+ |
let backend = Flatpak {
|
|
3062 |
+ |
remotes: || Some(Vec::new()),
|
|
3063 |
+ |
};
|
|
3064 |
+ |
let (_, entry) = spec.resolve("thing").unwrap();
|
|
3065 |
+ |
let err = backend.create("thing", entry).unwrap_err().to_string();
|
|
3066 |
+ |
assert!(err.contains("org.example.Thing"), "names the app: {err}");
|
|
3067 |
+ |
assert!(err.contains("remote-add"), "names what to run: {err}");
|
|
3068 |
+ |
}
|
|
3069 |
+ |
|
|
3070 |
+ |
// A box that declares its remote says where it comes from, so the probe is
|
|
3071 |
+ |
// not consulted and flatpak's own error is the right one if that remote is
|
|
3072 |
+ |
// missing.
|
|
3073 |
+ |
#[test]
|
|
3074 |
+ |
fn a_declared_remote_is_installed_from_without_asking_what_is_configured() {
|
|
3075 |
+ |
let spec = creation_spec();
|
|
3076 |
+ |
let backend = Flatpak {
|
|
3077 |
+ |
remotes: || panic!("the probe must not run when the spec names a remote"),
|
|
3078 |
+ |
};
|
|
3079 |
+ |
assert_eq!(
|
|
3080 |
+ |
create_argv(&backend, &spec, "inkscape"),
|
|
3081 |
+ |
"flatpak install --user --noninteractive flathub org.inkscape.Inkscape"
|
|
3082 |
+ |
);
|
|
3083 |
+ |
}
|
|
3084 |
+ |
|
|
3085 |
+ |
// A probe that could not run is not evidence of anything. Refusing on it
|
|
3086 |
+ |
// would turn a flatpak that failed to answer into an install the user
|
|
3087 |
+ |
// cannot make.
|
|
3088 |
+ |
#[test]
|
|
3089 |
+ |
fn an_unanswerable_probe_does_not_block_the_install() {
|
|
3090 |
+ |
let spec = Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.Thing\"\n")
|
|
3091 |
+ |
.unwrap();
|
|
3092 |
+ |
let backend = Flatpak { remotes: || None };
|
|
3093 |
+ |
assert_eq!(
|
|
3094 |
+ |
create_argv(&backend, &spec, "thing"),
|
|
3095 |
+ |
"flatpak install --user --noninteractive org.example.Thing"
|
|
3096 |
+ |
);
|
|
3097 |
+ |
}
|
|
3098 |
+ |
|
|
3099 |
+ |
// `--columns=name` prints one remote per line and nothing else; a machine
|
|
3100 |
+ |
// with none prints an empty body rather than a header.
|
|
3101 |
+ |
#[test]
|
|
3102 |
+ |
fn remotes_are_read_one_per_line() {
|
|
3103 |
+ |
assert_eq!(parse_remotes("fedora\nflathub\n"), ["fedora", "flathub"]);
|
|
3104 |
+ |
assert!(parse_remotes("\n \n").is_empty());
|
|
3105 |
+ |
}
|
|
3106 |
+ |
|
| 2976 |
3107 |
|
// The level decides which source field is required, so a spec that omits it
|
| 2977 |
3108 |
|
// is only wrong at creation time. The error has to name the box and the
|
| 2978 |
3109 |
|
// field, since the file may declare a dozen of them.
|
| 2991 |
3122 |
|
);
|
| 2992 |
3123 |
|
|
| 2993 |
3124 |
|
let (_, sandboxed) = spec.resolve("nothing").unwrap();
|
| 2994 |
|
- |
let err = Flatpak
|
|
3125 |
+ |
let err = flatpak()
|
| 2995 |
3126 |
|
.create("nothing", sandboxed)
|
| 2996 |
3127 |
|
.unwrap_err()
|
| 2997 |
3128 |
|
.to_string();
|
| 3061 |
3192 |
|
#[test]
|
| 3062 |
3193 |
|
fn each_level_has_exactly_one_backend() {
|
| 3063 |
3194 |
|
for level in [Level::Host, Level::Workspace, Level::Sandboxed] {
|
| 3064 |
|
- |
let implementors = [&Podman as &dyn Backend, &Flatpak]
|
|
3195 |
+ |
let implementors = [&Podman as &dyn Backend, &flatpak()]
|
| 3065 |
3196 |
|
.iter()
|
| 3066 |
3197 |
|
.filter(|backend| backend.implements(level))
|
| 3067 |
3198 |
|
.count();
|