Package touchHLE into a .app bundle on macOS by default

In order to support this, resource files are now looked up from the
bundle, and user data is now stored in Application Support. The old
behaviour of looking in the current directory is still supported if
the touchHLE executable is outside a bundle. Fixes #290.

Change-Id: Ib5b9b1316cdb910d8b2e1f18db1143d351b3bffb
This commit is contained in:
hikari_no_yume
2024-12-27 22:33:35 +01:00
parent ad6a5dff91
commit 86656465e5
6 changed files with 74 additions and 20 deletions

View File

@@ -48,11 +48,13 @@ jobs:
- name: Test
run: cargo test --target x86_64-apple-darwin
- name: Build
run: cargo build --target x86_64-apple-darwin --release && mv target/x86_64-apple-darwin/release/touchHLE .
run: cargo build --target x86_64-apple-darwin --release
- name: Create .app bundle
run: cd dev-scripts && ./make-macos-bundle.sh ../target/x86_64-apple-darwin/release/touchHLE && mkdir touchHLE-dmg-src && mv touchHLE.app touchHLE-dmg-src/ && hdiutil create -volname touchHLE -srcfolder touchHLE-dmg-src touchHLE.dmg && mv touchHLE.dmg ..
- uses: actions/upload-artifact@v3
with:
name: touchHLE_macOS_x86_64
path: touchHLE
path: touchHLE.dmg
- uses: actions/upload-artifact@v3
with:
name: TestApp_built_on_macOS

View File

@@ -46,6 +46,7 @@ Usability:
- App icons in the app picker are now sorted by the display name of the app, case-insensitively. (@hikari-no-yume)
- The accelerometer (tilt controls) can now be simulated using a mouse, instead of a game controller or real accelerometer. Simply hold down the right mouse button and move the mouse cursor. (@alborrajo)
- Default options for various games. (@nighto)
- macOS builds and releases of touchHLE now come as an application bundle (`.app` directory) rather than as a bare “Unix executable” file. This should fix problems some users encountered with running touchHLE outside of a terminal, and allows putting touchHLE in the Applications folder like a normal graphical app. To support this, user data (apps, options, etc) is now stored in “Application Support” rather than the current directory, and the bundled files (fonts, dylibs, etc) are now part of the app bundle. If you prefer the old layout, you can still get it if you move all the files out of the bundle. (@hikari-no-yume)
Quality:

View File

@@ -1 +1,2 @@
/new_release
/touchHLE.app

View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -e
if [[ $# == 1 ]]; then
PATH_TO_BINARY="$1"
shift
rm -rf touchHLE.app
mkdir -p touchHLE.app/Contents/MacOS touchHLE.app/Contents/Resources
cp $PATH_TO_BINARY touchHLE.app/Contents/MacOS/touchHLE
cp -r ../touchHLE_dylibs touchHLE.app/Contents/Resources/
cp -r ../touchHLE_fonts touchHLE.app/Contents/Resources/
cp -r ../touchHLE_default_options.txt touchHLE.app/Contents/Resources/
else
echo "Incorrect usage."
exit 1
fi

View File

@@ -135,10 +135,8 @@ pub fn main<T: Iterator<Item = String>>(mut args: T) -> Result<(), String> {
echo!();
{
let base_path = paths::user_data_base_path().to_str().unwrap();
if !base_path.is_empty() {
log!("Base path for touchHLE files: {}", base_path);
}
let base_path = paths::user_data_base_path();
log!("Base path for touchHLE files: {}", base_path.display());
paths::prepopulate_user_data_dir();
}

View File

@@ -21,8 +21,9 @@
//! See also [crate::fs], which provides a virtual filesystem for the guest app
//! and defines path types.
use std::borrow::Cow;
use std::io::{Read, Seek};
use std::path::Path;
use std::path::{Path, PathBuf};
/// Name of the directory containing ARMv6 dynamic libraries bundled with
/// touchHLE.
@@ -34,6 +35,21 @@ pub const FONTS_DIR: &str = "touchHLE_fonts";
/// Name of the file containing touchHLE's default options for various apps.
pub const DEFAULT_OPTIONS_FILE: &str = "touchHLE_default_options.txt";
/// macOS-only: If touchHLE is located in a .app bundle, return the path of the
/// Resources directory. If touchHLE is not located in a .app bundle, return
/// [None].
fn get_macos_bundled_resources_path() -> Option<PathBuf> {
if std::env::consts::OS != "macos" {
return None;
}
let base_path = PathBuf::from(sdl2::filesystem::base_path().ok()?);
if base_path.file_name().is_some_and(|p| p == "Resources") {
Some(base_path)
} else {
None
}
}
/// Abstraction over a platform-specific type for accessing a resource bundled
/// with touchHLE.
pub struct ResourceFile {
@@ -49,10 +65,15 @@ impl ResourceFile {
// APK. We access them via SDL2's wrapper of Android's assets API.
#[cfg(target_os = "android")]
file: sdl2::rwops::RWops::from_file(path, "r")?,
// On other OSes, resources are ordinary files in the current
// directory.
// On other OSes, resources are accessed as ordinary files.
#[cfg(not(target_os = "android"))]
file: std::fs::File::open(path).map_err(|e| e.to_string())?,
file: {
let base_path = get_macos_bundled_resources_path();
// When not in a bundle, look in the current directory.
let path = base_path.as_deref().unwrap_or(Path::new(".")).join(path);
std::fs::File::open(path).map_err(|e| e.to_string())?
},
})
}
pub fn get(&mut self) -> &mut (impl Read + Seek) {
@@ -90,7 +111,7 @@ pub const SANDBOX_DIR: &str = "touchHLE_sandbox";
/// Get a platform-specific base path needed for accessing touchHLE's
/// user-modifiable files. This is empty on platforms other than Android.
pub fn user_data_base_path() -> &'static Path {
pub fn user_data_base_path() -> Cow<'static, Path> {
#[cfg(target_os = "android")]
unsafe {
// This is an exception to the rule that SDL2 should only be used
@@ -110,10 +131,20 @@ pub fn user_data_base_path() -> &'static Path {
log!("Couldn't get Android external storage path!");
panic!();
}
Path::new(std::ffi::CStr::from_ptr(path).to_str().unwrap())
Cow::from(Path::new(std::ffi::CStr::from_ptr(path).to_str().unwrap()))
}
#[cfg(not(target_os = "android"))]
Path::new("")
{
// When touchHLE is run from a .app bundle on macOS, the user might not
// be able to control the current directory, so user data needs to go in
// a standard location.
if get_macos_bundled_resources_path().is_some() {
return Cow::from(PathBuf::from(
sdl2::filesystem::pref_path("touchhle.org", "touchHLE").unwrap(),
));
}
Cow::from(Path::new("."))
}
}
/// Get a URI that can be used to open a file manager or similar for the path
@@ -141,15 +172,19 @@ pub fn url_for_opening_user_data_dir() -> Result<String, String> {
}
}
/// Only meaningful on Android: create the user data directory if it doesn't
/// exist, and populate it with templates or README files. (On other platforms
/// these are simply bundled with touchHLE in a ZIP file.)
/// Only meaningful on certain OSes: create the user data directory if it
/// doesn't exist, and populate it with templates or README files. (On other
/// platforms these are simply bundled with touchHLE in a ZIP file.)
pub fn prepopulate_user_data_dir() {
if std::env::consts::OS != "android" {
if std::env::consts::OS != "android" && std::env::consts::OS != "macos" {
return;
}
let base_path = user_data_base_path();
if base_path == Path::new(".") {
return;
}
let apps_dir = user_data_base_path().join(APPS_DIR);
let apps_dir = base_path.join(APPS_DIR);
if !apps_dir.is_dir() {
match std::fs::create_dir(&apps_dir) {
Ok(()) => {
@@ -181,13 +216,13 @@ pub fn prepopulate_user_data_dir() {
create_file(&apps_dir_readme, content);
}
let user_options = user_data_base_path().join(USER_OPTIONS_FILE);
let user_options = base_path.join(USER_OPTIONS_FILE);
if !user_options.is_file() {
let content = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/touchHLE_options.txt"));
create_file(&user_options, content);
}
let options_help = user_data_base_path().join("OPTIONS_HELP.txt");
let options_help = base_path.join("OPTIONS_HELP.txt");
if !options_help.is_file() {
create_file(&options_help, crate::options::OPTIONS_HELP);
}