Skip to content
Merged
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
26 changes: 26 additions & 0 deletions installer/iris-gui-notarized.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Developer ID / notarized distribution (the DMG + CLI tarballs from
release.yml) — NOT the Mac App Store. These builds are signed with the
hardened runtime (codesign option `runtime`), which blocks executable
memory the kernel can't validate. IRIS's Cranelift JIT (the MIPS JIT and
the always-on REX3 draw-shader JIT) allocates code pages with
mmap+mprotect, which are exactly that, so without this entitlement the
first JITed REX3 draw is killed with SIGKILL/CODESIGNING.

allow-unsigned-executable-memory permits it and is allowed for
Developer ID + notarization. It is REJECTED for the Mac App Store, which
is why the App Store build (installer/iris-gui.entitlements, app-sandbox)
instead runs interpreter-only via IRIS_NO_JIT (set under the `appstore`
feature). See rules/jit/macos-app-sandbox-kills-cranelift-jit-no-map_jit.md.

No app-sandbox / app-identifier keys here: the notarized DMG is not
sandboxed and is not tied to an App Store provisioning profile. -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
15 changes: 11 additions & 4 deletions iris-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,17 @@ assets = [
]

[features]
# Mac App Store distribution build. Hides the CI/Automation config tab (the
# iris-ci socket is a developer automation feature unusable in the sandbox)
# and any other developer-only affordances. Set by .github/workflows/appstore.yml.
appstore = []
# Pre-compiled / distributed build. Hides developer-only affordances that don't
# make sense for users running an official binary — currently the iris.toml
# import/export menu items (the GUI's gui.json machine store is the system of
# record; iris.toml is only useful alongside the standalone `iris` CLI, i.e. a
# source checkout). Set by the Release and App Store workflows. A no-op for
# source builds, where the iris.toml workflow stays available.
bundled = []
# Mac App Store distribution build. Implies `bundled`, and additionally hides
# the CI/Automation config tab (the iris-ci socket is a developer automation
# feature unusable in the sandbox). Set by .github/workflows/appstore.yml.
appstore = ["bundled"]

[dependencies]
# Group A (additive) features are always on for iris-gui so the user can
Expand Down
54 changes: 44 additions & 10 deletions iris-gui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,59 @@ impl JitEnv {
}
}

pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv) {
/// Action a config tab asks the app to perform that needs app-level state
/// (e.g. a confirmation modal) the immediate-mode tab UI doesn't own.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConfigAction {
#[default]
None,
/// User clicked "Use embedded PROM"; the app should confirm with the user
/// and, if accepted, clear `cfg.prom` (an empty path falls back to the
/// built-in PROM in `iris::prom::Prom::from_file_or_embedded`).
RequestEmbeddedProm,
}

pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv) -> ConfigAction {
ScrollArea::vertical().show(ui, |ui| match tab {
Tab::General => show_general(ui, cfg),
Tab::Disks => show_disks(ui, cfg),
Tab::Network => show_network(ui, cfg),
Tab::Memory => show_memory(ui, cfg),
Tab::Display => show_display(ui, cfg),
Tab::VideoIn => show_vino(ui, cfg),
Tab::Debug => show_debug(ui, cfg, jit),
Tab::Ci => show_ci(ui, cfg),
});
Tab::Disks => { show_disks(ui, cfg); ConfigAction::None }
Tab::Network => { show_network(ui, cfg); ConfigAction::None }
Tab::Memory => { show_memory(ui, cfg); ConfigAction::None }
Tab::Display => { show_display(ui, cfg); ConfigAction::None }
Tab::VideoIn => { show_vino(ui, cfg); ConfigAction::None }
Tab::Debug => { show_debug(ui, cfg, jit); ConfigAction::None }
Tab::Ci => { show_ci(ui, cfg); ConfigAction::None }
}).inner
}

fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) {
fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction {
let mut action = ConfigAction::None;
ui.heading("General");
Grid::new("general_grid").num_columns(2).striped(true).show(ui, |ui| {
ui.label("PROM image");
path_row(ui, "prom", &mut cfg.prom, Pick::OpenFile, PROM_FILTERS);
ui.end_row();

// Leaving the PROM path empty boots the built-in PROM. Expose that as
// an explicit button so reverting from a (possibly missing) custom PROM
// is discoverable instead of "delete the text by hand". Disabled when
// already empty, so the confirm prompt only ever appears when a custom
// PROM is selected.
ui.label("");
ui.horizontal(|ui| {
let custom = !cfg.prom.is_empty();
if ui.add_enabled(custom, egui::Button::new("Use embedded PROM"))
.on_hover_text("Boot IRIS's built-in PROM instead of a file")
.clicked()
{
action = ConfigAction::RequestEmbeddedProm;
}
if !custom {
ui.label(RichText::new("(using built-in PROM)").weak());
}
});
ui.end_row();

ui.label("NVRAM file");
path_row(ui, "nvram", &mut cfg.nvram, Pick::SaveFile, NVRAM_FILTERS);
ui.end_row();
Expand All @@ -111,6 +144,7 @@ fn show_general(ui: &mut Ui, cfg: &mut MachineConfig) {
path_row_opt(ui, "serial_log", &mut cfg.serial_log, Pick::SaveFile, ANY_FILTERS);
ui.end_row();
});
action
}

fn show_memory(ui: &mut Ui, cfg: &mut MachineConfig) {
Expand Down
78 changes: 58 additions & 20 deletions iris-gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod scsi_menu;
mod settings;
mod single_instance;

use config_ui::{cfg_to_toml, show_tab, JitEnv, Tab};
use config_ui::{cfg_to_toml, show_tab, ConfigAction, JitEnv, Tab};
use dialogs::create_disk::CreateDiskDialog;
use dialogs::new_machine::{distribute_ram, NewMachineDialog};
use eframe::egui;
Expand Down Expand Up @@ -115,6 +115,8 @@ struct App {
fullscreen: bool,
stop_modal: Option<StopModal>,
missing_modal: Option<MissingDiskModal>,
/// Set when the user clicks "Use embedded PROM"; drives a confirmation modal.
confirm_embedded_prom: bool,
new_machine: NewMachineDialog,
create_disk: CreateDiskDialog,
/// If true, central panel shows the tabbed config editor; otherwise the
Expand Down Expand Up @@ -227,6 +229,7 @@ impl App {
toast: None,
stop_modal: None,
missing_modal: None,
confirm_embedded_prom: false,
new_machine,
create_disk: CreateDiskDialog::default(),
show_config_editor: false,
Expand Down Expand Up @@ -455,26 +458,32 @@ impl App {
}
ui.close_menu();
}
ui.separator();
if ui.button("Import iris.toml…").clicked() {
if let Some(path) = native_open_dialog("Import iris.toml", &[("TOML", &["toml"])]) {
let cfg = MachineConfig::load_toml(&path.to_string_lossy());
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("imported");
let name = self.prefs.unique_name(stem);
self.prefs.machines.insert(name.clone(), cfg.clone());
self.prefs.active_machine = Some(name.clone());
self.cfg = cfg;
self.cfg_path = Some(path);
self.flush_machine();
self.toast(format!("imported as '{name}'"));
// iris.toml import/export is a source-build affordance for users
// who also run the standalone `iris` CLI; the GUI's own gui.json
// machine store is the system of record. Hidden in pre-compiled /
// App Store builds (the `bundled` feature). See iris-gui Cargo.toml.
if !cfg!(feature = "bundled") {
ui.separator();
if ui.button("Import iris.toml…").clicked() {
if let Some(path) = native_open_dialog("Import iris.toml", &[("TOML", &["toml"])]) {
let cfg = MachineConfig::load_toml(&path.to_string_lossy());
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("imported");
let name = self.prefs.unique_name(stem);
self.prefs.machines.insert(name.clone(), cfg.clone());
self.prefs.active_machine = Some(name.clone());
self.cfg = cfg;
self.cfg_path = Some(path);
self.flush_machine();
self.toast(format!("imported as '{name}'"));
}
ui.close_menu();
}
ui.close_menu();
}
if ui.button("Export current to iris.toml…").clicked() {
if let Some(path) = native_save_dialog("Export iris.toml", &[("TOML", &["toml"])]) {
self.save_config(path);
if ui.button("Export current to iris.toml…").clicked() {
if let Some(path) = native_save_dialog("Export iris.toml", &[("TOML", &["toml"])]) {
self.save_config(path);
}
ui.close_menu();
}
ui.close_menu();
}
ui.separator();
if ui.button("Quit").clicked() {
Expand Down Expand Up @@ -754,7 +763,10 @@ impl App {
}
});
ui.separator();
show_tab(ui, self.tab, &mut self.cfg, &mut self.jit);
match show_tab(ui, self.tab, &mut self.cfg, &mut self.jit) {
ConfigAction::RequestEmbeddedProm => self.confirm_embedded_prom = true,
ConfigAction::None => {}
}
}

fn welcome_panel(&mut self, ui: &mut egui::Ui) {
Expand Down Expand Up @@ -977,6 +989,32 @@ impl eframe::App for App {
}
}

// Confirm switching from a custom PROM back to the built-in image.
if self.confirm_embedded_prom {
let prom = self.cfg.prom.clone();
let mut close = false;
let mut do_clear = false;
egui::Window::new("Use embedded PROM?")
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.label(RichText::new("Replace the selected PROM with IRIS's built-in PROM?").strong());
ui.label(RichText::new(&prom).weak());
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button("Cancel").clicked() { close = true; }
if ui.button("Use embedded PROM").clicked() { do_clear = true; close = true; }
});
});
if do_clear {
self.cfg.prom.clear();
self.mark_dirty();
self.toast("using embedded PROM");
}
if close { self.confirm_embedded_prom = false; }
}

// Missing-disk modal.
enum MissingChoice { None, Cancel, Detach, EditDisks }
let mut choice = MissingChoice::None;
Expand Down
Loading