From d9ad23fe5ea92b45c7f9976795e6ddc241b0fb60 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 23 Oct 2021 12:54:47 +0900 Subject: [PATCH 01/32] Add Windows implementations. --- Cargo.lock | 108 +++++++ Cargo.toml | 9 + build.rs | 27 ++ cargo-mobile-manifest.rc | 2 + cargo-mobile.exe.manifest | 40 +++ src/android/cli.rs | 2 +- src/android/device.rs | 9 +- src/android/jnilibs.rs | 22 +- src/android/ndk.rs | 16 +- src/android/project.rs | 8 +- src/env.rs | 18 ++ src/init.rs | 8 +- src/os/linux/mod.rs | 27 +- src/os/macos/mod.rs | 31 +- src/os/mod.rs | 8 +- src/os/windows/info.rs | 101 ++++++ src/os/windows/ln.rs | 50 +++ src/os/windows/mod.rs | 287 ++++++++++++++++++ src/util/cargo.rs | 4 +- src/util/ln.rs | 22 ++ src/util/path.rs | 13 +- .../android-studio/app/build.gradle.kts.hbs | 17 ++ 22 files changed, 795 insertions(+), 34 deletions(-) create mode 100644 cargo-mobile-manifest.rc create mode 100644 cargo-mobile.exe.manifest create mode 100644 src/os/windows/info.rs create mode 100644 src/os/windows/ln.rs create mode 100644 src/os/windows/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a6b79e91..b2ee4fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,8 +163,11 @@ dependencies = [ "bossy 0.2.1", "cocoa", "colored", + "const-utf16", "core-foundation", "deunicode", + "dunce", + "embed-resource", "english-numbers", "env_logger", "freedesktop_entry_parser", @@ -190,6 +193,7 @@ dependencies = [ "thiserror", "toml", "ureq", + "windows", "yes-or-no", ] @@ -258,6 +262,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "const-sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" + +[[package]] +name = "const-utf16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90feefab165fe011746e3be2f0708b7b180fcbd9f5054ff81a454d7bd93d8285" + [[package]] name = "core-foundation" version = "0.7.0" @@ -311,6 +327,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dunce" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" + +[[package]] +name = "embed-resource" +version = "1.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85505eb239fc952b300f29f0556d2d884082a83566768d980278d8faf38c780d" +dependencies = [ + "cc", + "vswhom", + "winreg", +] + [[package]] name = "encoding" version = "0.2.33" @@ -1290,6 +1323,26 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f5402d3d0e79a069714f7b48e3ecc60be7775a2c049cb839457457a239532" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "walkdir" version = "2.3.2" @@ -1415,6 +1468,61 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f5f8d2ea79bf690bbee453fd4a1516ae426e5d5c7215d96cc0c3dc134fc4a0" +dependencies = [ + "const-sha1", + "windows_gen", + "windows_macros", + "windows_reader", +] + +[[package]] +name = "windows_gen" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6994f42f8481387778cc608407d6703410672d57f32a66009419d7a18aa912" +dependencies = [ + "windows_quote", + "windows_reader", +] + +[[package]] +name = "windows_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cc2357b1b03c19f056cb0e6d06011f80f54beadb4e36aee2ca98493c7cfc3c" +dependencies = [ + "syn", + "windows_gen", + "windows_quote", + "windows_reader", +] + +[[package]] +name = "windows_quote" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf987b5288c15e1997226848f78f3ed3ef8b78dcfd71a201c8c8684163a7e4d" + +[[package]] +name = "windows_reader" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237b53e8b40766ea7db5da0d8c6c1442d21d0429f0ee7500d7b5688967bd9d7b" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wyz" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 99718b5d..4f9e843d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ bicycle = { git = "https://github.com/BrainiumLLC/bicycle", rev = "28080e0c6fa40 bossy = "0.2.1" colored = "1.9.3" deunicode = "1.1.1" +dunce = "1.0.2" english-numbers = "0.3.3" env_logger = "0.7.1" heck = "0.3.1" @@ -62,7 +63,15 @@ ureq = "2.2.0" freedesktop_entry_parser = "1.1" lexical-core = "0.7.6" +[target.'cfg(windows)'.dependencies] +const-utf16 = "0.2.1" +windows = "0.21.1" + [build-dependencies] bicycle = { git = "https://github.com/BrainiumLLC/bicycle", rev = "28080e0c6fa4067d9dd1b0f2b7322b6b32178e1f" } hit = "0.1.0" home = "0.5.3" + +[target.'cfg(windows)'.build-dependencies] +embed-resource = "1.6.4" +windows = "0.21.1" diff --git a/build.rs b/build.rs index a1f71749..007c4429 100644 --- a/build.rs +++ b/build.rs @@ -57,4 +57,31 @@ fn main() { ) .expect("failed to process actions"); } + + #[cfg(windows)] + { + // Embed application manifest + let resource_path = manifest_dir.join("cargo-mobile-manifest.rc"); + let manifest_path = manifest_dir.join("cargo-mobile.exe.manifest"); + println!("cargo:rerun-if-changed={}", resource_path.display()); + println!("cargo:rerun-if-changed={}", manifest_path.display()); + embed_resource::compile("cargo-mobile-manifest.rc"); + + // Build winapi bindings + windows::build! { + Windows::Win32::{ + Foundation::MAX_PATH, + System::{ + Diagnostics::Debug::{GetLastError, WIN32_ERROR}, + Memory::LocalFree, + SystemInformation::{VerifyVersionInfoW, VerSetConditionMask}, + SystemServices::{VER_EQUAL, VER_GREATER_EQUAL}, + Registry::HKEY_LOCAL_MACHINE, + + }, + UI::Shell::{ASSOCF_INIT_IGNOREUNKNOWN, AssocQueryStringW, CommandLineToArgvW, SHRegGetPathW}, + Storage::FileSystem::CreateHardLinkW, + }, + } + } } diff --git a/cargo-mobile-manifest.rc b/cargo-mobile-manifest.rc new file mode 100644 index 00000000..2ab79190 --- /dev/null +++ b/cargo-mobile-manifest.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "cargo-mobile.exe.manifest" diff --git a/cargo-mobile.exe.manifest b/cargo-mobile.exe.manifest new file mode 100644 index 00000000..edbe6b9f --- /dev/null +++ b/cargo-mobile.exe.manifest @@ -0,0 +1,40 @@ + + + + cargo-mobile + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/cli.rs b/src/android/cli.rs index 3bb24976..1394570f 100644 --- a/src/android/cli.rs +++ b/src/android/cli.rs @@ -86,7 +86,7 @@ pub enum Error { MetadataFailed(metadata::Error), Unsupported, ProjectDirAbsent { project_dir: PathBuf }, - OpenFailed(bossy::Error), + OpenFailed(os::OpenFileError), CheckFailed(CompileLibError), BuildFailed(BuildError), RunFailed(RunError), diff --git a/src/android/device.rs b/src/android/device.rs index a3449b48..0626c067 100644 --- a/src/android/device.rs +++ b/src/android/device.rs @@ -8,6 +8,7 @@ use super::{ use crate::{ env::ExplicitEnv as _, opts::{self, FilterLevel, NoiseLevel, Profile}, + os::{consts, gradlew_command}, util::{ self, cli::{Report, Reportable}, @@ -19,11 +20,7 @@ use std::{ }; fn gradlew(config: &Config, env: &Env) -> bossy::Command { - let gradlew_path = config.project_dir().join("gradlew"); - bossy::Command::pure(&gradlew_path) - .with_env_vars(env.explicit_env()) - .with_arg("--project-dir") - .with_arg(config.project_dir()) + gradlew_command(&config.project_dir()).with_env_vars(env.explicit_env()) } #[derive(Debug)] @@ -371,7 +368,7 @@ impl<'a> Device<'a> { pub fn stacktrace(&self, config: &Config, env: &Env) -> Result<(), StacktraceError> { // -d = print and exit let logcat_command = adb::adb(env, &self.serial_no).with_args(&["logcat", "-d"]); - let stack_command = bossy::Command::pure("ndk-stack") + let stack_command = bossy::Command::pure(env.ndk.home().join(consts::NDK_STACK)) .with_env_vars(env.explicit_env()) .with_env_var( "PATH", diff --git a/src/android/jnilibs.rs b/src/android/jnilibs.rs index fb285f6b..c9d10993 100644 --- a/src/android/jnilibs.rs +++ b/src/android/jnilibs.rs @@ -1,4 +1,6 @@ use super::{config::Config, target::Target}; +#[cfg(windows)] +use crate::os; use crate::{ target::TargetTrait as _, util::{ @@ -118,15 +120,17 @@ impl JniLibs { pub fn symlink_lib(&self, src: &Path) -> Result<(), SymlinkLibError> { log::info!("symlinking lib {:?} in jniLibs dir {:?}", src, self.path); if src.is_file() { - ln::force_symlink( - src, - self.path.join( - src.file_name() - .expect("developer error: file had no file name"), - ), - ln::TargetStyle::File, - ) - .map_err(SymlinkLibError::SymlinkFailed) + let dest = self.path.join( + src.file_name() + .expect("developer error: file had no file name"), + ); + #[cfg(not(windows))] + ln::force_symlink(src, &dest, ln::TargetStyle::File) + .map_err(SymlinkLibError::SymlinkFailed)?; + #[cfg(windows)] + os::ln::force_hard_link_or_copy_file(src, &dest) + .map_err(SymlinkLibError::SymlinkFailed)?; + Ok(()) } else { Err(SymlinkLibError::SourceMissing(src.to_owned())) } diff --git a/src/android/ndk.rs b/src/android/ndk.rs index 13c5abae..6bd6a7ea 100644 --- a/src/android/ndk.rs +++ b/src/android/ndk.rs @@ -2,7 +2,10 @@ use super::{ source_props::{self, SourceProps}, target::Target, }; -use crate::util::cli::{Report, Reportable}; +use crate::{ + os::consts, + util::cli::{Report, Reportable}, +}; use once_cell_regex::regex_multi_line; use std::{ collections::HashSet, @@ -45,8 +48,8 @@ pub enum Compiler { impl Compiler { fn as_str(&self) -> &'static str { match self { - Compiler::Clang => "clang", - Compiler::Clangxx => "clang++", + Compiler::Clang => consts::CLANG, + Compiler::Clangxx => consts::CLANGXX, } } } @@ -61,8 +64,8 @@ pub enum Binutil { impl Binutil { fn as_str(&self) -> &'static str { match self { - Binutil::Ar => "ar", - Binutil::Ld => "ld", + Binutil::Ar => consts::AR, + Binutil::Ld => consts::LD, } } } @@ -260,7 +263,8 @@ impl Env { fn readelf_path(&self, triple: &str) -> Result { MissingToolError::check_file( - self.tool_dir()?.join(format!("{}-readelf", triple)), + self.tool_dir()? + .join(format!("{}-{}", triple, consts::READELF)), "readelf", ) } diff --git a/src/android/project.rs b/src/android/project.rs index a675e850..f9662882 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -6,6 +6,7 @@ use super::{ }; use crate::{ dot_cargo, + os::replace_path_separator, target::TargetTrait as _, templating::{self, Pack}, util::{ @@ -93,7 +94,10 @@ pub fn gen( |map| { map.insert( "root-dir-rel", - util::relativize_path(config.app().root_dir(), config.project_dir()), + replace_path_separator( + util::relativize_path(config.app().root_dir(), config.project_dir()) + .into_os_string(), + ), ); map.insert("root-dir", config.app().root_dir()); map.insert("targets", Target::all().values().collect::>()); @@ -128,6 +132,7 @@ pub fn gen( .map(|p| p.name.as_str()) .collect::>(), ); + map.insert("windows", cfg!(windows)); }, filter.fun(), ) @@ -175,6 +180,7 @@ pub fn gen( path: dest.clone(), cause, })?; + #[cfg(not(windows))] ln::force_symlink_relative(config.app().asset_dir(), dest, ln::TargetStyle::Directory) .map_err(Error::AssetDirSymlinkFailed)?; diff --git a/src/env.rs b/src/env.rs index a09f60cb..539c590d 100644 --- a/src/env.rs +++ b/src/env.rs @@ -26,6 +26,9 @@ pub struct Env { path: String, term: Option, ssh_auth_sock: Option, + system_root: Option, + tmp: Option, + temp: Option, } impl Env { @@ -34,11 +37,17 @@ impl Env { let path = std::env::var("PATH").map_err(Error::PathNotSet)?; let term = std::env::var("TERM").ok(); let ssh_auth_sock = std::env::var("SSH_AUTH_SOCK").ok(); + let system_root = std::env::var("SystemRoot").ok(); + let tmp = std::env::var("TMP").ok(); + let temp = std::env::var("TEMP").ok(); Ok(Self { home, path, term, ssh_auth_sock, + system_root, + tmp, + temp, }) } @@ -61,6 +70,15 @@ impl ExplicitEnv for Env { if let Some(ssh_auth_sock) = self.ssh_auth_sock.as_ref() { env.push(("SSH_AUTH_SOCK", ssh_auth_sock.as_ref())); } + if let Some(system_root) = self.system_root.as_ref() { + env.push(("SystemRoot", system_root.as_ref())); + } + if let Some(tmp) = self.tmp.as_ref() { + env.push(("TMP", tmp.as_ref())); + } + if let Some(temp) = self.temp.as_ref() { + env.push(("TEMP", temp.as_ref())); + } env } } diff --git a/src/init.rs b/src/init.rs index 635645f9..b21b5d3d 100644 --- a/src/init.rs +++ b/src/init.rs @@ -7,7 +7,9 @@ use crate::{ metadata::{self, Metadata}, Config, }, - dot_cargo, opts, project, templating, + dot_cargo, opts, + os::code_command, + project, templating, util::{ self, cli::{Report, Reportable, TextWrapper}, @@ -134,8 +136,8 @@ pub fn exec( if skip_dev_tools.no() && util::command_present("code").map_err(Error::CodeCommandPresentFailed)? { - let mut command = bossy::Command::impure("code") - .with_args(&["--install-extension", "vadimcn.vscode-lldb"]); + let mut command = code_command(); + command.add_args(&["--install-extension", "vadimcn.vscode-lldb"]); if non_interactive.yes() { command.add_arg("--force"); } diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs index e1adff1a..50cd315e 100644 --- a/src/os/linux/mod.rs +++ b/src/os/linux/mod.rs @@ -125,7 +125,7 @@ impl Application { pub fn open_file_with( application: impl AsRef, path: impl AsRef, -) -> bossy::Result<()> { +) -> Result<(), OpenFileError> { let app_str = application.as_ref(); let path_str = path.as_ref(); @@ -164,6 +164,7 @@ pub fn open_file_with( bossy::Command::impure(&command_parts[0]) .with_args(&command_parts[1..]) .run_and_detach() + .map_err(OpenFileError::LaunchFailed) } // We use "sh" in order to access "command -v", as that is a bultin command on sh. @@ -174,3 +175,27 @@ pub fn command_path(name: &str) -> bossy::Result { .with_args(&["-c", &format!("command -v {}", name)]) .run_and_wait_for_output() } + +pub fn code_command() -> bossy::Command { + bossy::Command::impure("code") +} + +pub fn gradlew_command(project_dir: impl AsRef) -> bossy::Command { + let gradle_path = Path::new(project_dir.as_ref()).join("gradlew"); + bossy::Command::impure(&gradle_path) + .with_arg("--project-dir") + .with_arg(&project_dir) +} + +pub fn replace_path_separator(path: OsString) -> OsString { + path +} + +pub mod consts { + pub const AR: &str = "ar"; + pub const CLANG: &str = "clang"; + pub const CLANGXX: &str = "clang++"; + pub const LD: &str = "ld"; + pub const READELF: &str = "readelf"; + pub const NDK_STACK: &str = "ndk-stack"; +} diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs index 9c9ba8af..62676cb4 100644 --- a/src/os/macos/mod.rs +++ b/src/os/macos/mod.rs @@ -35,6 +35,7 @@ impl Display for DetectEditorError { pub enum OpenFileError { PathToUrlFailed { path: PathBuf }, LaunchFailed(OSStatus), + BossyLaouchFailed(bossy::Error), } impl Display for OpenFileError { @@ -44,6 +45,7 @@ impl Display for OpenFileError { write!(f, "Failed to convert path {:?} into a `CFURL`.", path) } Self::LaunchFailed(status) => write!(f, "Status code {}", status), + Self::BossyLaunchFailed(e) => write!(f, "Launch failed: {}", e), } } } @@ -96,11 +98,12 @@ impl Application { pub fn open_file_with( application: impl AsRef, path: impl AsRef, -) -> bossy::Result<()> { +) -> Result<(), OpenFileError> { bossy::Command::impure("open") .with_arg("-a") .with_args(&[application.as_ref(), path.as_ref()]) - .run_and_wait()?; + .run_and_wait() + .map_err(OpenFileError::BossyLaunchFailed)?; Ok(()) } @@ -110,3 +113,27 @@ pub fn command_path(name: &str) -> bossy::Result { .with_args(&["-v", name]) .run_and_wait_for_output() } + +pub fn code_command() -> bossy::Command { + bossy::Command::impure("code") +} + +pub fn gradlew_command(project_dir: impl AsRef) -> bossy::Command { + let gradle_path = Path::new(project_dir.as_ref()).join("gradlew"); + bossy::Command::impure(&gradle_path) + .with_arg("--project-dir") + .with_arg(&project_dir) +} + +pub fn replace_path_separator(path: OsString) -> OsString { + path +} + +pub mod consts { + pub const AR: &str = "ar"; + pub const CLANG: &str = "clang"; + pub const CLANGXX: &str = "clang++"; + pub const LD: &str = "ld"; + pub const READELF: &str = "readelf"; + pub const NDK_STACK: &str = "ndk-stack"; +} diff --git a/src/os/mod.rs b/src/os/mod.rs index cd9f9a16..244f60bf 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -12,7 +12,13 @@ mod linux; #[cfg(target_os = "linux")] pub use self::linux::*; -#[cfg(not(any(target_os = "macos", target_os = "linux")))] +#[cfg(windows)] +mod windows; + +#[cfg(windows)] +pub use self::windows::*; + +#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] compile_error!("Host platform not yet supported by cargo-mobile! We'd love if you made a PR to add support for this platform ❤️"); // TODO: we should probably expose common functionality throughout `os` in a diff --git a/src/os/windows/info.rs b/src/os/windows/info.rs new file mode 100644 index 00000000..32a9ad2c --- /dev/null +++ b/src/os/windows/info.rs @@ -0,0 +1,101 @@ +use super::Windows::Win32::System::{ + SystemInformation::{ + VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_BUILDNUMBER, + VER_MAJORVERSION, VER_MINORVERSION, VER_PRODUCT_TYPE, VER_SERVICEPACKMAJOR, + }, + SystemServices::{VER_EQUAL, VER_GREATER_EQUAL}, +}; +use crate::os::Info; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to find Version")] + VersionMissing, +} + +// (name, major_version, minor_version, service_pack, product_type, build_number) +const VERSION_LIST: &[(&str, u32, u32, u16, u8, Option)] = &[ + ("Windows 11", 10, 0, 0, 1, Some(22000)), + ("Windows 10", 10, 0, 0, 1, None), + ("Windows Server 2019", 10, 0, 0, 3, Some(17623)), + ("Windows Server 2016", 10, 0, 0, 3, None), + ("Windows 8.1", 6, 3, 0, 1, None), + ("Windows Server 2012 R2", 6, 3, 0, 3, None), + ("Windows 8", 6, 2, 0, 1, None), + ("Windows Server 2012", 6, 2, 0, 3, None), + ("Windows 7 Service Pack 1", 6, 1, 1, 1, None), + ("Windows 7", 6, 1, 0, 1, None), + ("Windows Server 2008 R2 Service Pack 1", 6, 1, 1, 3, None), + ("Windows Server 2008 R2", 6, 1, 0, 3, None), + ("Windows Server 2008", 6, 0, 0, 3, None), + ("Windows Vista Service Pack 2", 6, 0, 2, 1, None), + ("Windows Vista Service Pack 1", 6, 0, 1, 1, None), + ("Windows Vista", 6, 0, 0, 1, None), + // How to identify Windows Server 2003 R2 is unknown. + ("Windows Server 2003 Service Pack 2", 5, 2, 2, 3, None), + ("Windows Server 2003 Service Pack 1", 5, 2, 1, 3, None), + ("Windows Server 2003", 5, 2, 0, 3, None), + ("Windows XP 64-Bit Edition", 5, 2, 0, 1, None), + ("Windows XP Service Pack 3", 5, 1, 3, 1, None), + ("Windows XP Service Pack 2", 5, 1, 2, 1, None), + ("Windows XP Service Pack 1", 5, 1, 1, 1, None), + ("Windows XP", 5, 1, 0, 1, None), + ("Windows 2000", 5, 0, 0, 1, None), + // Older versions are omitted. +]; + +pub fn check() -> Result { + let mut osvi = OSVERSIONINFOEXW { + dwOSVersionInfoSize: std::mem::size_of::() as _, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + wServicePackMajor: 0, + wServicePackMinor: 0, + wSuiteMask: 0, + wProductType: 0, + wReserved: 0, + }; + let condition_mask = + unsafe { VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL as u8) }; + let condition_mask = + unsafe { VerSetConditionMask(condition_mask, VER_MINORVERSION, VER_GREATER_EQUAL as u8) }; + let condition_mask = unsafe { + VerSetConditionMask( + condition_mask, + VER_SERVICEPACKMAJOR, + VER_GREATER_EQUAL as u8, + ) + }; + let condition_mask = + unsafe { VerSetConditionMask(condition_mask, VER_PRODUCT_TYPE, VER_EQUAL as u8) }; + let type_mask = VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR | VER_PRODUCT_TYPE; + + for &(name, major, minor, service_pack, product_type, build_number) in VERSION_LIST { + osvi.dwMajorVersion = major; + osvi.dwMinorVersion = minor; + osvi.wServicePackMajor = service_pack; + osvi.wProductType = product_type; + let (condition_mask, type_mask) = if let Some(build_number) = build_number { + let condition_mask = unsafe { + VerSetConditionMask(condition_mask, VER_BUILDNUMBER, VER_GREATER_EQUAL as u8) + }; + let type_mask = type_mask | VER_BUILDNUMBER; + osvi.dwBuildNumber = build_number; + (condition_mask, type_mask) + } else { + (condition_mask, type_mask) + }; + if unsafe { VerifyVersionInfoW(&mut osvi as *mut _, type_mask, condition_mask) }.as_bool() { + return Ok(Info { + name: name.to_string(), + version: format!("{}.{}", major, minor), + }); + }; + } + + Err(Error::VersionMissing) +} diff --git a/src/os/windows/ln.rs b/src/os/windows/ln.rs new file mode 100644 index 00000000..8f17f446 --- /dev/null +++ b/src/os/windows/ln.rs @@ -0,0 +1,50 @@ +use super::Windows::Win32::{Foundation::PWSTR, Storage::FileSystem::CreateHardLinkW}; +use crate::util::ln::{Clobber, Error, ErrorCause, LinkType, TargetStyle}; +use std::{os::windows::ffi::OsStrExt, path::Path}; + +pub fn force_hard_link_or_copy_file( + src: impl AsRef, + dest: impl AsRef, +) -> Result<(), Error> { + let src = src.as_ref(); + let dest = dest.as_ref(); + let mut src_buf = src.as_os_str().encode_wide().collect::>(); + let mut dest_buf = dest.as_os_str().encode_wide().collect::>(); + src_buf.push(0); + dest_buf.push(0); + if dest.is_file() { + std::fs::remove_file(dest).map_err(|err| { + Error::new( + LinkType::Hard, + Clobber::FileOnly, + src.to_owned(), + dest.to_owned(), + TargetStyle::File, + ErrorCause::IOError(err), + ) + })?; + } + if !unsafe { + CreateHardLinkW( + PWSTR(dest_buf.as_mut_ptr()), + PWSTR(src_buf.as_mut_ptr()), + std::ptr::null_mut(), + ) + } + .as_bool() + { + // If the drive is different between src and dest, the creation of the hard link will fail. + // In that case, fall back to the simple copy. + std::fs::copy(src, dest).map_err(|err| { + Error::new( + LinkType::Hard, + Clobber::FileOnly, + src.to_owned(), + dest.to_owned(), + TargetStyle::File, + ErrorCause::IOError(err), + ) + })?; + } + Ok(()) +} diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs new file mode 100644 index 00000000..c5290989 --- /dev/null +++ b/src/os/windows/mod.rs @@ -0,0 +1,287 @@ +pub(super) mod info; +pub mod ln; + +use std::{ + ffi::{OsStr, OsString}, + os::windows::ffi::{OsStrExt, OsStringExt}, + path::Path, + slice::from_raw_parts, +}; +use thiserror::Error; + +windows::include_bindings!(); + +use Windows::Win32::{ + Foundation::{MAX_PATH, PWSTR}, + System::{ + Diagnostics::Debug::ERROR_NO_ASSOCIATION, Diagnostics::Debug::ERROR_SUCCESS, + Memory::LocalFree, Registry::HKEY_LOCAL_MACHINE, + }, + UI::Shell::{ + AssocQueryStringW, CommandLineToArgvW, SHRegGetPathW, ASSOCF_INIT_IGNOREUNKNOWN, + ASSOCSTR_COMMAND, + }, +}; + +#[derive(Debug, Error)] +pub enum DetectEditorError { + #[error("No default editor is set: AssocQueryStringW for \".rs\" and \".txt\" both failed")] + NoDefaultEditorSet, + #[error("An error occured while calling AssocQueryStringW: {0}")] + OSError(#[source] windows::Error), +} + +impl From for DetectEditorError { + fn from(err: windows::Error) -> Self { + Self::OSError(err) + } +} + +#[derive(Debug, Error)] +pub enum OpenFileError { + #[error("Launch Failed: {0}")] + LaunchFailed(#[source] bossy::Error), + #[error("An error occured while calling OS API: {0}")] + OSError(#[source] windows::Error), +} + +pub struct Application { + argv: Vec, +} + +const RUST_EXT: &[u16] = const_utf16::encode_null_terminated!(".rs"); +const TEXT_EXT: &[u16] = const_utf16::encode_null_terminated!(".txt"); + +impl Application { + pub fn detect_editor() -> Result { + let editor_command = Self::detect_associated_command(RUST_EXT).or_else(|e| match e { + DetectEditorError::NoDefaultEditorSet => Self::detect_associated_command(TEXT_EXT), + err => Err(err), + })?; + let argv: Vec<_> = NativeArgv::new(&editor_command).into(); + + Ok(Self { argv }) + } + + pub fn open_file(&self, path: impl AsRef) -> Result<(), OpenFileError> { + let args = self.argv[1..] + .iter() + .map(|arg| Self::replace_command_arg(arg, &path.as_ref().as_os_str())) + .collect::>(); + bossy::Command::impure(&self.argv[0]) + .add_args(&args) + .run_and_detach() + .map_err(OpenFileError::LaunchFailed) + } + + fn detect_associated_command(ext: &[u16]) -> Result, DetectEditorError> { + let mut len: u32 = 0; + if let Err(e) = unsafe { + AssocQueryStringW( + ASSOCF_INIT_IGNOREUNKNOWN as u32, + ASSOCSTR_COMMAND, + // In Shlwapi.h, this parameter's type is `LPCWSTR`. + // So it's not modified actually. + PWSTR(ext.as_ptr() as _), + PWSTR::default(), + PWSTR::default(), + &mut len as _, + ) + } { + if e.code().0 == 0x80070000 | ERROR_NO_ASSOCIATION.0 { + return Err(DetectEditorError::NoDefaultEditorSet); + } + return Err(DetectEditorError::OSError(e)); + } + let mut command: Vec = vec![0; len as usize]; + unsafe { + AssocQueryStringW( + ASSOCF_INIT_IGNOREUNKNOWN as u32, + ASSOCSTR_COMMAND, + // In Shlwapi.h, this parameter's type is `LPCWSTR`. + // So it's not modified actually. + PWSTR(RUST_EXT.as_ptr() as _), + PWSTR::default(), + PWSTR(command.as_mut_ptr()), + &mut len as _, + ) + }?; + return Ok(command); + } + + // Replace %0 or %1 to arg1, and other % is unescape + fn replace_command_arg(arg: &OsStr, arg1: &OsStr) -> OsString { + let mut is_percent = false; + let mut iter = arg.encode_wide(); + let mut buffer = vec![]; + const ZERO: u16 = '0' as u16; + const ONE: u16 = '1' as u16; + const TWO: u16 = '2' as u16; + const NINE: u16 = '9' as u16; + const PERCENT: u16 = '%' as u16; + loop { + match (iter.next(), is_percent) { + (Some(ZERO..=ONE), true) => { + buffer.extend(arg1.encode_wide()); + } + (Some(TWO..=NINE), true) => { + // Nothing to do. + } + (Some(PERCENT), false) => { + is_percent = true; + continue; + } + (Some(c), _) => { + buffer.push(c); + } + (None, _) => break, + } + is_percent = false; + } + OsString::from_wide(&buffer) + } +} + +pub fn open_file_with( + application: impl AsRef, + path: impl AsRef, +) -> Result<(), OpenFileError> { + // In windows, there is no standerd way to find application by name. + match application.as_ref().to_str() { + Some("Android Studio") => open_file_with_android_studio(path), + _ => { + unimplemented!() + } + } +} + +const ANDROID_STUDIO_UNINSTALL_KEY_PATH: &[u16] = const_utf16::encode_null_terminated!( + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Android Studio" +); +const ANDROID_STUDIO_UNINSTALLER_VALUE: &[u16] = + const_utf16::encode_null_terminated!("UninstallString"); +#[cfg(target_pointer_width = "64")] +const STUDIO_EXE_PATH: &str = "bin/studio64.exe"; +#[cfg(target_pointer_width = "32")] +const STUDIO_EXE_PATH: &str = "bin/studio.exe"; + +fn open_file_with_android_studio(path: impl AsRef) -> Result<(), OpenFileError> { + let mut buffer = [0; MAX_PATH as usize]; + let lstatus = unsafe { + SHRegGetPathW( + HKEY_LOCAL_MACHINE, + PWSTR(ANDROID_STUDIO_UNINSTALL_KEY_PATH.as_ptr() as _), + PWSTR(ANDROID_STUDIO_UNINSTALLER_VALUE.as_ptr() as _), + PWSTR(buffer.as_mut_ptr()), + 0, + ) + }; + if lstatus.0 as u32 != ERROR_SUCCESS.0 { + return Err(OpenFileError::OSError(windows::Error::from_win32())); + } + let len = NullTerminatedWTF16Iterator(buffer.as_slice().as_ptr()).count(); + let uninstaller_path = OsString::from_wide(&buffer[..len]); + let application_path = Path::new(&uninstaller_path) + .parent() + .expect("failed to getAndroid Studio uninstaller's parent path") + .join(STUDIO_EXE_PATH); + bossy::Command::impure(application_path) + .add_arg( + dunce::canonicalize(Path::new(path.as_ref())) + .expect("Failed to canonicalize file path"), + ) + .run_and_wait() + .map_err(OpenFileError::LaunchFailed)?; + Ok(()) +} + +pub fn command_path(name: &str) -> bossy::Result { + bossy::Command::impure("where.exe") + .add_arg(name) + .run_and_wait_for_output() +} + +struct NativeArgv { + argv: *mut PWSTR, + len: i32, +} + +impl NativeArgv { + // The buffer must be null terminated. + fn new(buffer: &[u16]) -> Self { + let mut len = 0; + // In shellap.h, lpcmdline's type is `LPCWSTR`. + // So it's not modified actually. + let argv = unsafe { CommandLineToArgvW(PWSTR(buffer.as_ptr() as _), &mut len as _) }; + Self { argv, len } + } +} + +impl Drop for NativeArgv { + fn drop(&mut self) { + unsafe { LocalFree(self.argv as _) }; + } +} + +impl From for Vec { + fn from(native_argv: NativeArgv) -> Self { + let mut argv = Vec::with_capacity(native_argv.len as usize); + let argv_slice = unsafe { from_raw_parts(native_argv.argv, native_argv.len as _) }; + for pwstr in argv_slice { + let len = NullTerminatedWTF16Iterator(pwstr.0).count(); + let arg = OsString::from_wide(unsafe { std::slice::from_raw_parts(pwstr.0, len) }); + argv.push(arg); + } + argv + } +} + +struct NullTerminatedWTF16Iterator(*const u16); + +impl Iterator for NullTerminatedWTF16Iterator { + type Item = u16; + fn next(&mut self) -> Option { + match unsafe { *self.0 } { + 0 => None, + c => { + self.0 = unsafe { self.0.offset(1) }; + Some(c) + } + } + } +} + +// Directly invoking code.cmd behaves strangely. +// For example, if running `cargo mobile new foo` in C:\Users\MyHome, +// %~dp0 will expand to C:\Users\MyHome\foo in code.cmd, which is completely broken. +// Running it through powershell.exe does not have this problem. +pub fn code_command() -> bossy::Command { + bossy::Command::impure("powershell.exe").with_args(&["-Command", "code"]) +} + +pub fn gradlew_command(project_dir: impl AsRef) -> bossy::Command { + // Path without verbatim prefix. + let project_dir = dunce::canonicalize(Path::new(project_dir.as_ref())) + .expect("Failed to canonicalize project dir"); + let gradlew_path = project_dir.join("gradlew.bat"); + bossy::Command::impure(&gradlew_path) + .with_arg("--project-dir") + .with_arg(&project_dir) +} + +pub fn replace_path_separator(path: OsString) -> OsString { + let buf = path + .encode_wide() + .map(|c| if c == '\\' as u16 { '/' as u16 } else { c }) + .collect::>(); + OsString::from_wide(&buf) +} + +pub mod consts { + pub const AR: &str = "ar.exe"; + pub const CLANG: &str = "clang.cmd"; + pub const CLANGXX: &str = "clang++.cmd"; + pub const LD: &str = "ld.exe"; + pub const READELF: &str = "readelf.exe"; + pub const NDK_STACK: &str = "ndk-stack.cmd"; +} diff --git a/src/util/cargo.rs b/src/util/cargo.rs index 1f4db566..8e3798f5 100644 --- a/src/util/cargo.rs +++ b/src/util/cargo.rs @@ -38,7 +38,9 @@ impl<'a> CargoCommand<'a> { } pub fn with_manifest_path(mut self, manifest_path: Option) -> Self { - self.manifest_path = manifest_path; + self.manifest_path = manifest_path.map(|manifest_path| { + dunce::canonicalize(manifest_path).expect("Failed to canonicalize manifest path") + }); self } diff --git a/src/util/ln.rs b/src/util/ln.rs index 927cf078..bdeebc80 100644 --- a/src/util/ln.rs +++ b/src/util/ln.rs @@ -54,6 +54,7 @@ impl Display for TargetStyle { pub enum ErrorCause { MissingFileName, CommandFailed(bossy::Error), + IOError(std::io::Error), } impl Display for ErrorCause { @@ -63,6 +64,7 @@ impl Display for ErrorCause { write!(f, "Neither the source nor target contained a file name.",) } Self::CommandFailed(err) => write!(f, "`ln` command failed: {}", err), + Self::IOError(err) => write!(f, "IO error: {}", err), } } } @@ -77,6 +79,26 @@ pub struct Error { cause: ErrorCause, } +impl Error { + pub fn new( + link_type: LinkType, + force: Clobber, + source: PathBuf, + target: PathBuf, + target_style: TargetStyle, + cause: ErrorCause, + ) -> Self { + Self { + link_type, + force, + source, + target, + target_style, + cause, + } + } +} + impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( diff --git a/src/util/path.rs b/src/util/path.rs index 69e54bd4..fff8a249 100644 --- a/src/util/path.rs +++ b/src/util/path.rs @@ -39,9 +39,16 @@ pub fn contract_home(path: impl AsRef) -> Result Result { diff --git a/templates/platforms/android-studio/app/build.gradle.kts.hbs b/templates/platforms/android-studio/app/build.gradle.kts.hbs index dde026cf..8f2cc67c 100644 --- a/templates/platforms/android-studio/app/build.gradle.kts.hbs +++ b/templates/platforms/android-studio/app/build.gradle.kts.hbs @@ -61,12 +61,29 @@ dependencies { implementation("{{this}}"){{/each}} } +{{#if windows}}tasks.register("syncAssets") { + val root = "{{root-dir-rel}}../assets" + val absoluteRoot = file(root).absoluteFile + into("src/main/assets") + fileTree(root).forEach() { file -> + if (file.isFile) { + val relativeDir = file.parentFile.toRelativeString(absoluteRoot) + from(file) { + into(relativeDir) + } + } + } +}{{/if}} + afterEvaluate { android.applicationVariants.all { val buildType = "${buildType.name.capitalize()}" productFlavors.forEach { val archAndBuildType = name.capitalize() tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"]) + {{#if windows}} + tasks["rustBuild${archAndBuildType}"].dependsOn(tasks["syncAssets"]) + tasks["merge${archAndBuildType}Assets"].dependsOn("syncAssets"){{/if}} } } } From cd3b2101bc3d9058d1c9fc2812b40539dc1c8e75 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 23 Oct 2021 18:33:26 +0900 Subject: [PATCH 02/32] Fix several environment variable error. --- src/env.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/env.rs b/src/env.rs index 539c590d..7c26ed56 100644 --- a/src/env.rs +++ b/src/env.rs @@ -22,32 +22,42 @@ impl Reportable for Error { #[derive(Clone, Debug)] pub struct Env { - home: String, + home: Option, + userprofile: Option, path: String, term: Option, ssh_auth_sock: Option, system_root: Option, tmp: Option, temp: Option, + program_data: Option, } impl Env { pub fn new() -> Result { - let home = std::env::var("HOME").map_err(Error::HomeNotSet)?; + let home = std::env::var("HOME"); + let userprofile = std::env::var("USERPROFILE"); + let (home, userprofile) = match (home, userprofile) { + (Err(home_err), Err(_)) => return Err(Error::HomeNotSet(home_err)), + (home, userprofile) => (home.ok(), userprofile.ok()), + }; let path = std::env::var("PATH").map_err(Error::PathNotSet)?; let term = std::env::var("TERM").ok(); let ssh_auth_sock = std::env::var("SSH_AUTH_SOCK").ok(); let system_root = std::env::var("SystemRoot").ok(); let tmp = std::env::var("TMP").ok(); let temp = std::env::var("TEMP").ok(); + let program_data = std::env::var("ProgramData").ok(); Ok(Self { home, + userprofile, path, term, ssh_auth_sock, system_root, tmp, temp, + program_data, }) } @@ -63,7 +73,13 @@ impl Env { impl ExplicitEnv for Env { fn explicit_env(&self) -> Vec<(&str, &std::ffi::OsStr)> { - let mut env = vec![("HOME", self.home.as_ref()), ("PATH", self.path.as_ref())]; + let mut env = vec![("PATH", self.path.as_ref())]; + if let Some(home) = self.home.as_ref() { + env.push(("HOME", home.as_ref())); + } + if let Some(userprofile) = self.userprofile.as_ref() { + env.push(("USERPROFILE", userprofile.as_ref())); + } if let Some(term) = self.term.as_ref() { env.push(("TERM", term.as_ref())); } @@ -79,6 +95,9 @@ impl ExplicitEnv for Env { if let Some(temp) = self.temp.as_ref() { env.push(("TEMP", temp.as_ref())); } + if let Some(program_data) = self.program_data.as_ref() { + env.push(("ProgramData", program_data.as_ref())); + } env } } From a44d566ffa146e4ce009954c0230329c96346ab3 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 23 Oct 2021 18:53:34 +0900 Subject: [PATCH 03/32] Fix generating build.gradle.kts bug. --- src/android/project.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/android/project.rs b/src/android/project.rs index f9662882..0b7294b1 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -16,7 +16,10 @@ use crate::{ }, }; use path_abs::PathOps; -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; pub static TEMPLATE_PACK: &str = "android-studio"; pub static ASSET_PACK_TEMPLATE_PACK: &str = "android-studio-asset-pack"; @@ -94,10 +97,10 @@ pub fn gen( |map| { map.insert( "root-dir-rel", - replace_path_separator( + Path::new(&replace_path_separator( util::relativize_path(config.app().root_dir(), config.project_dir()) .into_os_string(), - ), + )), ); map.insert("root-dir", config.app().root_dir()); map.insert("targets", Target::all().values().collect::>()); From 99c2c050069114b22baea0e9c382197fc70cb493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Mon, 25 Oct 2021 19:46:54 +0200 Subject: [PATCH 04/32] add egui example --- README.md | 1 + templates/apps/egui/.gitignore.hbs | 10 + templates/apps/egui/Cargo.toml.hbs | 29 +++ templates/apps/egui/README.md | 5 + templates/apps/egui/gen/bin/desktop.rs.hbs | 4 + templates/apps/egui/src/lib.rs | 207 +++++++++++++++++++++ 6 files changed, 256 insertions(+) create mode 100644 templates/apps/egui/.gitignore.hbs create mode 100644 templates/apps/egui/Cargo.toml.hbs create mode 100644 templates/apps/egui/README.md create mode 100644 templates/apps/egui/gen/bin/desktop.rs.hbs create mode 100644 templates/apps/egui/src/lib.rs diff --git a/README.md b/README.md index 8cd2b6ad..25e70dbe 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ After some straightforward prompts, you'll be asked to select a template pack. T | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | bevy | Minimal Bevy project derived from [sprite](https://github.com/bevyengine/bevy/blob/master/examples/2d/sprite.rs) example | | bevy-demo | Bevy [breakout](https://github.com/bevyengine/bevy/blob/master/examples/game/breakout.rs) demo | +| egui | Full egui + winit + wgpu example inspired by [egui_example](https://github.com/hasenbanck/egui_example) | | wgpu | Minimal wgpu project derived from [hello-triangle](https://github.com/gfx-rs/wgpu-rs/tree/master/examples/hello-triangle) example | | winit | Minimal winit project derived from [window](https://github.com/rust-windowing/winit/tree/master/examples/window) example | diff --git a/templates/apps/egui/.gitignore.hbs b/templates/apps/egui/.gitignore.hbs new file mode 100644 index 00000000..c3cb0ce4 --- /dev/null +++ b/templates/apps/egui/.gitignore.hbs @@ -0,0 +1,10 @@ +# Rust +target/ +**/*.rs.bk + +# cargo-mobile +.cargo/ +/gen + +# macOS +.DS_Store diff --git a/templates/apps/egui/Cargo.toml.hbs b/templates/apps/egui/Cargo.toml.hbs new file mode 100644 index 00000000..344ebe10 --- /dev/null +++ b/templates/apps/egui/Cargo.toml.hbs @@ -0,0 +1,29 @@ +[package] +name = "{{app.name}}" +version = "0.1.0" +authors = ["{{author}}"] +edition = "2018" +resolver = "2" + +[lib] +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "{{app.name}}-desktop" +path = "gen/bin/desktop.rs" + +[dependencies] +egui_wgpu_backend = { git = "https://github.com/hasenbanck/egui_wgpu_backend.git", rev = "961125e7bd2c71c5ead1d61a7ca7ffa8c0d17f48" } +chrono = "0.4" +pollster = "0.2" +egui = "0.15" +epi = "0.15" +wgpu = "0.11" +winit = "0.25" +egui_demo_lib = "0.15" +mobile-entry-point = "0.1" +egui-winit = "0.15" + +[target.'cfg(target_os = "android")'.dependencies] +log = "0.4.11" +ndk-glue = "0.3.0" diff --git a/templates/apps/egui/README.md b/templates/apps/egui/README.md new file mode 100644 index 00000000..b1544ec5 --- /dev/null +++ b/templates/apps/egui/README.md @@ -0,0 +1,5 @@ +# egui + +This is an example inspired by [this repo](https://github.com/hasenbanck/egui_example), using `egui`, `winit` and `wgpu` to run [egui_demo_app](https://github.com/emilk/egui/tree/master/egui_demo_app). + +To run this on desktop, just do `cargo run` like normal! For mobile, use `cargo android run` and `cargo apple run` respectively (or use `cargo android open` and `cargo apple open` to open in Android Studio and Xcode respectively). \ No newline at end of file diff --git a/templates/apps/egui/gen/bin/desktop.rs.hbs b/templates/apps/egui/gen/bin/desktop.rs.hbs new file mode 100644 index 00000000..df976373 --- /dev/null +++ b/templates/apps/egui/gen/bin/desktop.rs.hbs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + {{snake-case app.name}}::start_app(); +} diff --git a/templates/apps/egui/src/lib.rs b/templates/apps/egui/src/lib.rs new file mode 100644 index 00000000..123696c1 --- /dev/null +++ b/templates/apps/egui/src/lib.rs @@ -0,0 +1,207 @@ +use std::iter; +use std::time::Instant; + +use epi::*; +use winit::event_loop::ControlFlow; +use winit::event::{Event::*}; +use egui_wgpu_backend::{RenderPass, ScreenDescriptor}; + +use mobile_entry_point::mobile_entry_point; +#[cfg(target_os = "android")] +use ndk_glue; + +/// A custom event type for the winit app. +#[derive(Debug)] +enum Event { + RequestRedraw, +} + +/// This is the repaint signal type that egui needs for requesting a repaint from another thread. +/// It sends the custom RequestRedraw event to the winit event loop. +struct ExampleRepaintSignal(std::sync::Mutex>); + +impl epi::RepaintSignal for ExampleRepaintSignal { + fn request_repaint(&self) { + self.0.lock().unwrap().send_event(Event::RequestRedraw).ok(); + } +} + +#[cfg(target_os = "android")] +fn wait_for_native_screen() { + log::info!("App started. Waiting for NativeScreen"); + loop { + match ndk_glue::native_window().as_ref() { + Some(_) => { + log::info!("NativeScreen Found:{:?}", ndk_glue::native_window()); + break; + } + None => (), + } + } +} + +/// A simple egui + wgpu + winit based example. +#[mobile_entry_point] +fn main() { + let event_loop = winit::event_loop::EventLoop::with_user_event(); + let window = winit::window::WindowBuilder::new() + .with_decorations(true) + .with_transparent(false) + .with_title("A fantastic window!") + .with_inner_size(winit::dpi::LogicalSize { + width: 128.0, + height: 128.0, + }) + .build(&event_loop) + .unwrap(); + + #[cfg(target_os = "android")] + wait_for_native_screen(); + + let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); + + let surface = unsafe { instance.create_surface(&window) }; + + // WGPU 0.11+ support force fallback (if HW implementation not supported), set it to true or false (optional). + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .unwrap(); + + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::default(), + limits: wgpu::Limits::default(), + label: None, + }, + None, + )) + .unwrap(); + + let size = window.inner_size(); + let surface_format = surface.get_preferred_format(&adapter).unwrap(); + let mut surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width as u32, + height: size.height as u32, + present_mode: wgpu::PresentMode::Mailbox, + }; + surface.configure(&device, &surface_config); + + let repaint_signal = std::sync::Arc::new(ExampleRepaintSignal(std::sync::Mutex::new( + event_loop.create_proxy(), + ))); + + + // We use the egui_wgpu_backend crate as the render backend. + let mut egui_rpass = RenderPass::new(&device, surface_format, 1); + + // Display the demo application that ships with egui. + let mut demo_app = egui_demo_lib::WrapApp::default(); + + let mut previous_frame_time = None; + + let mut state = egui_winit::State::new(&window); + let mut ctx = egui::CtxRef::default(); + + event_loop.run(move |event, _, control_flow| { + let mut redraw = || { + let output_frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("Dropped frame with error: {}", e); + return; + } + }; + let output_view = output_frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let egui_start = Instant::now(); + let raw_input: egui::RawInput = state.take_egui_input(&window); + ctx.begin_frame(raw_input); + let mut app_output = epi::backend::AppOutput::default(); + + let mut frame = epi::backend::FrameBuilder { + info: epi::IntegrationInfo { + name: "egui_winit", + web_info: None, + cpu_usage: previous_frame_time, + native_pixels_per_point: Some(window.scale_factor() as _), + prefer_dark_mode: None, + }, + tex_allocator: &mut egui_rpass, + output: &mut app_output, + repaint_signal: repaint_signal.clone(), + } + .build(); + + // Draw the demo application. + demo_app.update(&ctx, &mut frame); + + // End the UI frame. We could now handle the output and draw the UI with the backend. + let (_output, paint_commands) = ctx.end_frame(); + let paint_jobs = ctx.tessellate(paint_commands); + + let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; + previous_frame_time = Some(frame_time); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("encoder"), + }); + + // Upload all resources for the GPU. + let screen_descriptor = ScreenDescriptor { + physical_width: surface_config.width, + physical_height: surface_config.height, + scale_factor: window.scale_factor() as f32, + }; + egui_rpass.update_texture(&device, &queue, &ctx.texture()); + egui_rpass.update_user_textures(&device, &queue); + egui_rpass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor); + + // Record all render passes. + egui_rpass + .execute( + &mut encoder, + &output_view, + &paint_jobs, + &screen_descriptor, + Some(wgpu::Color::BLACK), + ) + .unwrap(); + // Submit the commands. + queue.submit(iter::once(encoder.finish())); + + // Redraw egui + output_frame.present(); + }; + match event { + RedrawRequested(..) | UserEvent(Event::RequestRedraw) => { + redraw(); + } + MainEventsCleared => { + repaint_signal.request_repaint(); + } + WindowEvent { event, .. } => { + match event { + winit::event::WindowEvent::Resized(size) => { + surface_config.width = size.width; + surface_config.height = size.height; + surface.configure(&device, &surface_config); + }, + winit::event::WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; + }, + _ => () + }; + state.on_event(&ctx, &event); + repaint_signal.request_repaint(); + }, + _ => (), + } + }); +} From 549adddbfa9d1544cfec28b8a607c8cf4e46de7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Mon, 25 Oct 2021 21:50:36 +0200 Subject: [PATCH 05/32] fix egui template on desktop --- templates/apps/egui/src/lib.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/templates/apps/egui/src/lib.rs b/templates/apps/egui/src/lib.rs index 123696c1..16896718 100644 --- a/templates/apps/egui/src/lib.rs +++ b/templates/apps/egui/src/lib.rs @@ -48,9 +48,9 @@ fn main() { .with_decorations(true) .with_transparent(false) .with_title("A fantastic window!") - .with_inner_size(winit::dpi::LogicalSize { - width: 128.0, - height: 128.0, + .with_inner_size(winit::dpi::PhysicalSize { + width: 1280.0, + height: 720.0, }) .build(&event_loop) .unwrap(); @@ -108,6 +108,8 @@ fn main() { let mut ctx = egui::CtxRef::default(); event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + let mut redraw = || { let output_frame = match surface.get_current_texture() { Ok(frame) => frame, @@ -180,12 +182,9 @@ fn main() { output_frame.present(); }; match event { - RedrawRequested(..) | UserEvent(Event::RequestRedraw) => { + RedrawRequested(..) | UserEvent(Event::RequestRedraw) | MainEventsCleared => { redraw(); } - MainEventsCleared => { - repaint_signal.request_repaint(); - } WindowEvent { event, .. } => { match event { winit::event::WindowEvent::Resized(size) => { @@ -196,10 +195,10 @@ fn main() { winit::event::WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; }, - _ => () + _ => { + state.on_event(&ctx, &event); + } }; - state.on_event(&ctx, &event); - repaint_signal.request_repaint(); }, _ => (), } From ff8916eaf39f205c23d3a7d872327bc56e7e5d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Thu, 28 Oct 2021 23:16:49 +0200 Subject: [PATCH 06/32] bump egui_wgpu_backend to 0.14 in egui template --- templates/apps/egui/Cargo.toml.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/apps/egui/Cargo.toml.hbs b/templates/apps/egui/Cargo.toml.hbs index 344ebe10..27400d26 100644 --- a/templates/apps/egui/Cargo.toml.hbs +++ b/templates/apps/egui/Cargo.toml.hbs @@ -13,7 +13,7 @@ name = "{{app.name}}-desktop" path = "gen/bin/desktop.rs" [dependencies] -egui_wgpu_backend = { git = "https://github.com/hasenbanck/egui_wgpu_backend.git", rev = "961125e7bd2c71c5ead1d61a7ca7ffa8c0d17f48" } +egui_wgpu_backend = "0.14" chrono = "0.4" pollster = "0.2" egui = "0.15" From a394f85ee69dfcddcaff83171f82ce4366681c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Sat, 30 Oct 2021 11:01:40 +0200 Subject: [PATCH 07/32] egui template: handle Resumed/Suspended events, start without a surface on Android --- templates/apps/egui/Cargo.toml.hbs | 4 + templates/apps/egui/src/lib.rs | 210 ++++++++++++++++------------- 2 files changed, 121 insertions(+), 93 deletions(-) diff --git a/templates/apps/egui/Cargo.toml.hbs b/templates/apps/egui/Cargo.toml.hbs index 27400d26..2a0ae01f 100644 --- a/templates/apps/egui/Cargo.toml.hbs +++ b/templates/apps/egui/Cargo.toml.hbs @@ -25,5 +25,9 @@ mobile-entry-point = "0.1" egui-winit = "0.15" [target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.10" log = "0.4.11" ndk-glue = "0.3.0" + +[target.'cfg(not(target_os = "android"))'.dependencies] +simple_logger = "1.11.0" diff --git a/templates/apps/egui/src/lib.rs b/templates/apps/egui/src/lib.rs index 16896718..62a7bda1 100644 --- a/templates/apps/egui/src/lib.rs +++ b/templates/apps/egui/src/lib.rs @@ -16,6 +16,9 @@ enum Event { RequestRedraw, } +static INITIAL_WIDTH: u32 = 1280; +static INITIAL_HEIGHT: u32 = 720; + /// This is the repaint signal type that egui needs for requesting a repaint from another thread. /// It sends the custom RequestRedraw event to the winit event loop. struct ExampleRepaintSignal(std::sync::Mutex>); @@ -27,45 +30,42 @@ impl epi::RepaintSignal for ExampleRepaintSignal { } #[cfg(target_os = "android")] -fn wait_for_native_screen() { - log::info!("App started. Waiting for NativeScreen"); - loop { - match ndk_glue::native_window().as_ref() { - Some(_) => { - log::info!("NativeScreen Found:{:?}", ndk_glue::native_window()); - break; - } - None => (), - } - } +fn init_logging() { + android_logger::init_once( + android_logger::Config::default() + .with_min_level(log::Level::Trace) + .with_tag("{{app.name}}"), + ); +} + +#[cfg(not(target_os = "android"))] +fn init_logging() { + simple_logger::SimpleLogger::new().init().unwrap(); } /// A simple egui + wgpu + winit based example. #[mobile_entry_point] fn main() { + init_logging(); let event_loop = winit::event_loop::EventLoop::with_user_event(); let window = winit::window::WindowBuilder::new() - .with_decorations(true) - .with_transparent(false) .with_title("A fantastic window!") - .with_inner_size(winit::dpi::PhysicalSize { - width: 1280.0, - height: 720.0, - }) + .with_inner_size(winit::dpi::LogicalSize::new(INITIAL_WIDTH, INITIAL_HEIGHT)) .build(&event_loop) .unwrap(); - #[cfg(target_os = "android")] - wait_for_native_screen(); - let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); - let surface = unsafe { instance.create_surface(&window) }; + let mut surface = if cfg!(target_os = "android") { + None + } else { + Some(unsafe { instance.create_surface(&window) }) + }; // WGPU 0.11+ support force fallback (if HW implementation not supported), set it to true or false (optional). let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), + compatible_surface: surface.as_ref(), force_fallback_adapter: false, })) .unwrap(); @@ -80,16 +80,24 @@ fn main() { )) .unwrap(); - let size = window.inner_size(); - let surface_format = surface.get_preferred_format(&adapter).unwrap(); + let surface_format = if let Some(surface) = &surface { + surface.get_preferred_format(&adapter).unwrap() + } else { + // if Surface is none, we're guaranteed to be on android + wgpu::TextureFormat::Rgba8UnormSrgb + }; + let mut surface_config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, - width: size.width as u32, - height: size.height as u32, + width: INITIAL_WIDTH, + height: INITIAL_HEIGHT, present_mode: wgpu::PresentMode::Mailbox, }; - surface.configure(&device, &surface_config); + + if let Some(surface) = &mut surface { + surface.configure(&device, &surface_config); + } let repaint_signal = std::sync::Arc::new(ExampleRepaintSignal(std::sync::Mutex::new( event_loop.create_proxy(), @@ -111,86 +119,102 @@ fn main() { *control_flow = ControlFlow::Wait; let mut redraw = || { - let output_frame = match surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("Dropped frame with error: {}", e); - return; + if let Some(surface) = &surface { + let output_frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("Dropped frame with error: {}", e); + return; + } + }; + let output_view = output_frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let egui_start = Instant::now(); + let raw_input: egui::RawInput = state.take_egui_input(&window); + ctx.begin_frame(raw_input); + let mut app_output = epi::backend::AppOutput::default(); + + let mut frame = epi::backend::FrameBuilder { + info: epi::IntegrationInfo { + name: "egui_winit", + web_info: None, + cpu_usage: previous_frame_time, + native_pixels_per_point: Some(window.scale_factor() as _), + prefer_dark_mode: None, + }, + tex_allocator: &mut egui_rpass, + output: &mut app_output, + repaint_signal: repaint_signal.clone(), } - }; - let output_view = output_frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let egui_start = Instant::now(); - let raw_input: egui::RawInput = state.take_egui_input(&window); - ctx.begin_frame(raw_input); - let mut app_output = epi::backend::AppOutput::default(); - - let mut frame = epi::backend::FrameBuilder { - info: epi::IntegrationInfo { - name: "egui_winit", - web_info: None, - cpu_usage: previous_frame_time, - native_pixels_per_point: Some(window.scale_factor() as _), - prefer_dark_mode: None, - }, - tex_allocator: &mut egui_rpass, - output: &mut app_output, - repaint_signal: repaint_signal.clone(), - } - .build(); + .build(); - // Draw the demo application. - demo_app.update(&ctx, &mut frame); + // Draw the demo application. + demo_app.update(&ctx, &mut frame); - // End the UI frame. We could now handle the output and draw the UI with the backend. - let (_output, paint_commands) = ctx.end_frame(); - let paint_jobs = ctx.tessellate(paint_commands); + // End the UI frame. We could now handle the output and draw the UI with the backend. + let (_output, paint_commands) = ctx.end_frame(); + let paint_jobs = ctx.tessellate(paint_commands); - let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; - previous_frame_time = Some(frame_time); + let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; + previous_frame_time = Some(frame_time); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("encoder"), - }); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("encoder"), + }); - // Upload all resources for the GPU. - let screen_descriptor = ScreenDescriptor { - physical_width: surface_config.width, - physical_height: surface_config.height, - scale_factor: window.scale_factor() as f32, + // Upload all resources for the GPU. + let screen_descriptor = ScreenDescriptor { + physical_width: surface_config.width, + physical_height: surface_config.height, + scale_factor: window.scale_factor() as f32, + }; + egui_rpass.update_texture(&device, &queue, &ctx.texture()); + egui_rpass.update_user_textures(&device, &queue); + egui_rpass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor); + + // Record all render passes. + egui_rpass + .execute( + &mut encoder, + &output_view, + &paint_jobs, + &screen_descriptor, + Some(wgpu::Color::BLACK), + ) + .unwrap(); + // Submit the commands. + queue.submit(iter::once(encoder.finish())); + + // Redraw egui + output_frame.present(); }; - egui_rpass.update_texture(&device, &queue, &ctx.texture()); - egui_rpass.update_user_textures(&device, &queue); - egui_rpass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor); - - // Record all render passes. - egui_rpass - .execute( - &mut encoder, - &output_view, - &paint_jobs, - &screen_descriptor, - Some(wgpu::Color::BLACK), - ) - .unwrap(); - // Submit the commands. - queue.submit(iter::once(encoder.finish())); - - // Redraw egui - output_frame.present(); }; match event { - RedrawRequested(..) | UserEvent(Event::RequestRedraw) | MainEventsCleared => { + RedrawRequested(..) | MainEventsCleared => { redraw(); } + Resumed => { + let s = unsafe { instance.create_surface(&window) }; + surface_config.format = s.get_preferred_format(&adapter).unwrap(); + s.configure(&device, &surface_config); + surface = Some(s); + } + Suspended => { + surface = None; + } WindowEvent { event, .. } => { match event { winit::event::WindowEvent::Resized(size) => { - surface_config.width = size.width; - surface_config.height = size.height; - surface.configure(&device, &surface_config); + if size.width != 0 && size.height != 0 { + // Recreate the swap chain with the new size + surface_config.width = size.width; + surface_config.height = size.height; + if let Some(surface) = &surface { + surface.configure(&device, &surface_config); + } + } }, winit::event::WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; From a6783d3cf46f60ff65079ead247d1553464e01e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Sun, 31 Oct 2021 00:54:48 +0200 Subject: [PATCH 08/32] enable fullscreen in android by default --- .../platforms/android-studio/app/src/main/res/values/styles.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/platforms/android-studio/app/src/main/res/values/styles.xml b/templates/platforms/android-studio/app/src/main/res/values/styles.xml index 9785e0c9..e47a20d8 100644 --- a/templates/platforms/android-studio/app/src/main/res/values/styles.xml +++ b/templates/platforms/android-studio/app/src/main/res/values/styles.xml @@ -3,6 +3,7 @@ From dda51230a37619faeaa8433cffe575dc1b291f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Wed, 3 Nov 2021 18:41:54 +0100 Subject: [PATCH 09/32] enable windows in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d94e0a36..0584b24a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: matrix: rust_version: [stable, beta, nightly] platform: - # - { target: x86_64-pc-windows-msvc, os: windows-latest } + - { target: x86_64-pc-windows-msvc, os: windows-latest } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest } - { target: x86_64-apple-darwin, os: macos-latest } From 4e7e2d4d3c2b160fe247976e89f400474ce6a30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Wed, 3 Nov 2021 19:00:10 +0100 Subject: [PATCH 10/32] revert the accidental commits from another PR --- README.md | 1 - templates/apps/egui/.gitignore.hbs | 10 - templates/apps/egui/Cargo.toml.hbs | 33 --- templates/apps/egui/README.md | 5 - templates/apps/egui/gen/bin/desktop.rs.hbs | 4 - templates/apps/egui/src/lib.rs | 230 ------------------ .../app/src/main/res/values/styles.xml | 1 - 7 files changed, 284 deletions(-) delete mode 100644 templates/apps/egui/.gitignore.hbs delete mode 100644 templates/apps/egui/Cargo.toml.hbs delete mode 100644 templates/apps/egui/README.md delete mode 100644 templates/apps/egui/gen/bin/desktop.rs.hbs delete mode 100644 templates/apps/egui/src/lib.rs diff --git a/README.md b/README.md index 25e70dbe..8cd2b6ad 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ After some straightforward prompts, you'll be asked to select a template pack. T | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | bevy | Minimal Bevy project derived from [sprite](https://github.com/bevyengine/bevy/blob/master/examples/2d/sprite.rs) example | | bevy-demo | Bevy [breakout](https://github.com/bevyengine/bevy/blob/master/examples/game/breakout.rs) demo | -| egui | Full egui + winit + wgpu example inspired by [egui_example](https://github.com/hasenbanck/egui_example) | | wgpu | Minimal wgpu project derived from [hello-triangle](https://github.com/gfx-rs/wgpu-rs/tree/master/examples/hello-triangle) example | | winit | Minimal winit project derived from [window](https://github.com/rust-windowing/winit/tree/master/examples/window) example | diff --git a/templates/apps/egui/.gitignore.hbs b/templates/apps/egui/.gitignore.hbs deleted file mode 100644 index c3cb0ce4..00000000 --- a/templates/apps/egui/.gitignore.hbs +++ /dev/null @@ -1,10 +0,0 @@ -# Rust -target/ -**/*.rs.bk - -# cargo-mobile -.cargo/ -/gen - -# macOS -.DS_Store diff --git a/templates/apps/egui/Cargo.toml.hbs b/templates/apps/egui/Cargo.toml.hbs deleted file mode 100644 index 2a0ae01f..00000000 --- a/templates/apps/egui/Cargo.toml.hbs +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "{{app.name}}" -version = "0.1.0" -authors = ["{{author}}"] -edition = "2018" -resolver = "2" - -[lib] -crate-type = ["staticlib", "cdylib", "rlib"] - -[[bin]] -name = "{{app.name}}-desktop" -path = "gen/bin/desktop.rs" - -[dependencies] -egui_wgpu_backend = "0.14" -chrono = "0.4" -pollster = "0.2" -egui = "0.15" -epi = "0.15" -wgpu = "0.11" -winit = "0.25" -egui_demo_lib = "0.15" -mobile-entry-point = "0.1" -egui-winit = "0.15" - -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.10" -log = "0.4.11" -ndk-glue = "0.3.0" - -[target.'cfg(not(target_os = "android"))'.dependencies] -simple_logger = "1.11.0" diff --git a/templates/apps/egui/README.md b/templates/apps/egui/README.md deleted file mode 100644 index b1544ec5..00000000 --- a/templates/apps/egui/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# egui - -This is an example inspired by [this repo](https://github.com/hasenbanck/egui_example), using `egui`, `winit` and `wgpu` to run [egui_demo_app](https://github.com/emilk/egui/tree/master/egui_demo_app). - -To run this on desktop, just do `cargo run` like normal! For mobile, use `cargo android run` and `cargo apple run` respectively (or use `cargo android open` and `cargo apple open` to open in Android Studio and Xcode respectively). \ No newline at end of file diff --git a/templates/apps/egui/gen/bin/desktop.rs.hbs b/templates/apps/egui/gen/bin/desktop.rs.hbs deleted file mode 100644 index df976373..00000000 --- a/templates/apps/egui/gen/bin/desktop.rs.hbs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - {{snake-case app.name}}::start_app(); -} diff --git a/templates/apps/egui/src/lib.rs b/templates/apps/egui/src/lib.rs deleted file mode 100644 index 62a7bda1..00000000 --- a/templates/apps/egui/src/lib.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::iter; -use std::time::Instant; - -use epi::*; -use winit::event_loop::ControlFlow; -use winit::event::{Event::*}; -use egui_wgpu_backend::{RenderPass, ScreenDescriptor}; - -use mobile_entry_point::mobile_entry_point; -#[cfg(target_os = "android")] -use ndk_glue; - -/// A custom event type for the winit app. -#[derive(Debug)] -enum Event { - RequestRedraw, -} - -static INITIAL_WIDTH: u32 = 1280; -static INITIAL_HEIGHT: u32 = 720; - -/// This is the repaint signal type that egui needs for requesting a repaint from another thread. -/// It sends the custom RequestRedraw event to the winit event loop. -struct ExampleRepaintSignal(std::sync::Mutex>); - -impl epi::RepaintSignal for ExampleRepaintSignal { - fn request_repaint(&self) { - self.0.lock().unwrap().send_event(Event::RequestRedraw).ok(); - } -} - -#[cfg(target_os = "android")] -fn init_logging() { - android_logger::init_once( - android_logger::Config::default() - .with_min_level(log::Level::Trace) - .with_tag("{{app.name}}"), - ); -} - -#[cfg(not(target_os = "android"))] -fn init_logging() { - simple_logger::SimpleLogger::new().init().unwrap(); -} - -/// A simple egui + wgpu + winit based example. -#[mobile_entry_point] -fn main() { - init_logging(); - let event_loop = winit::event_loop::EventLoop::with_user_event(); - let window = winit::window::WindowBuilder::new() - .with_title("A fantastic window!") - .with_inner_size(winit::dpi::LogicalSize::new(INITIAL_WIDTH, INITIAL_HEIGHT)) - .build(&event_loop) - .unwrap(); - - let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); - - let mut surface = if cfg!(target_os = "android") { - None - } else { - Some(unsafe { instance.create_surface(&window) }) - }; - - // WGPU 0.11+ support force fallback (if HW implementation not supported), set it to true or false (optional). - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: surface.as_ref(), - force_fallback_adapter: false, - })) - .unwrap(); - - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - features: wgpu::Features::default(), - limits: wgpu::Limits::default(), - label: None, - }, - None, - )) - .unwrap(); - - let surface_format = if let Some(surface) = &surface { - surface.get_preferred_format(&adapter).unwrap() - } else { - // if Surface is none, we're guaranteed to be on android - wgpu::TextureFormat::Rgba8UnormSrgb - }; - - let mut surface_config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_format, - width: INITIAL_WIDTH, - height: INITIAL_HEIGHT, - present_mode: wgpu::PresentMode::Mailbox, - }; - - if let Some(surface) = &mut surface { - surface.configure(&device, &surface_config); - } - - let repaint_signal = std::sync::Arc::new(ExampleRepaintSignal(std::sync::Mutex::new( - event_loop.create_proxy(), - ))); - - - // We use the egui_wgpu_backend crate as the render backend. - let mut egui_rpass = RenderPass::new(&device, surface_format, 1); - - // Display the demo application that ships with egui. - let mut demo_app = egui_demo_lib::WrapApp::default(); - - let mut previous_frame_time = None; - - let mut state = egui_winit::State::new(&window); - let mut ctx = egui::CtxRef::default(); - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - let mut redraw = || { - if let Some(surface) = &surface { - let output_frame = match surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("Dropped frame with error: {}", e); - return; - } - }; - let output_view = output_frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - let egui_start = Instant::now(); - let raw_input: egui::RawInput = state.take_egui_input(&window); - ctx.begin_frame(raw_input); - let mut app_output = epi::backend::AppOutput::default(); - - let mut frame = epi::backend::FrameBuilder { - info: epi::IntegrationInfo { - name: "egui_winit", - web_info: None, - cpu_usage: previous_frame_time, - native_pixels_per_point: Some(window.scale_factor() as _), - prefer_dark_mode: None, - }, - tex_allocator: &mut egui_rpass, - output: &mut app_output, - repaint_signal: repaint_signal.clone(), - } - .build(); - - // Draw the demo application. - demo_app.update(&ctx, &mut frame); - - // End the UI frame. We could now handle the output and draw the UI with the backend. - let (_output, paint_commands) = ctx.end_frame(); - let paint_jobs = ctx.tessellate(paint_commands); - - let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; - previous_frame_time = Some(frame_time); - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("encoder"), - }); - - // Upload all resources for the GPU. - let screen_descriptor = ScreenDescriptor { - physical_width: surface_config.width, - physical_height: surface_config.height, - scale_factor: window.scale_factor() as f32, - }; - egui_rpass.update_texture(&device, &queue, &ctx.texture()); - egui_rpass.update_user_textures(&device, &queue); - egui_rpass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor); - - // Record all render passes. - egui_rpass - .execute( - &mut encoder, - &output_view, - &paint_jobs, - &screen_descriptor, - Some(wgpu::Color::BLACK), - ) - .unwrap(); - // Submit the commands. - queue.submit(iter::once(encoder.finish())); - - // Redraw egui - output_frame.present(); - }; - }; - match event { - RedrawRequested(..) | MainEventsCleared => { - redraw(); - } - Resumed => { - let s = unsafe { instance.create_surface(&window) }; - surface_config.format = s.get_preferred_format(&adapter).unwrap(); - s.configure(&device, &surface_config); - surface = Some(s); - } - Suspended => { - surface = None; - } - WindowEvent { event, .. } => { - match event { - winit::event::WindowEvent::Resized(size) => { - if size.width != 0 && size.height != 0 { - // Recreate the swap chain with the new size - surface_config.width = size.width; - surface_config.height = size.height; - if let Some(surface) = &surface { - surface.configure(&device, &surface_config); - } - } - }, - winit::event::WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - }, - _ => { - state.on_event(&ctx, &event); - } - }; - }, - _ => (), - } - }); -} diff --git a/templates/platforms/android-studio/app/src/main/res/values/styles.xml b/templates/platforms/android-studio/app/src/main/res/values/styles.xml index e47a20d8..9785e0c9 100644 --- a/templates/platforms/android-studio/app/src/main/res/values/styles.xml +++ b/templates/platforms/android-studio/app/src/main/res/values/styles.xml @@ -3,7 +3,6 @@ From c1ded18f1a5b2f1bf55b8c736f883c038d9a9d99 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Thu, 4 Nov 2021 20:58:49 +0900 Subject: [PATCH 11/32] Fix build errors. --- src/os/macos/mod.rs | 2 +- src/os/windows/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs index 62676cb4..01c54eb0 100644 --- a/src/os/macos/mod.rs +++ b/src/os/macos/mod.rs @@ -35,7 +35,7 @@ impl Display for DetectEditorError { pub enum OpenFileError { PathToUrlFailed { path: PathBuf }, LaunchFailed(OSStatus), - BossyLaouchFailed(bossy::Error), + BossyLaunchFailed(bossy::Error), } impl Display for OpenFileError { diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs index c5290989..bc59e0f8 100644 --- a/src/os/windows/mod.rs +++ b/src/os/windows/mod.rs @@ -179,7 +179,7 @@ fn open_file_with_android_studio(path: impl AsRef) -> Result<(), OpenFile if lstatus.0 as u32 != ERROR_SUCCESS.0 { return Err(OpenFileError::OSError(windows::Error::from_win32())); } - let len = NullTerminatedWTF16Iterator(buffer.as_slice().as_ptr()).count(); + let len = NullTerminatedWTF16Iterator(buffer.as_ptr()).count(); let uninstaller_path = OsString::from_wide(&buffer[..len]); let application_path = Path::new(&uninstaller_path) .parent() From 0f40b7b299bcee163c53481f83e50067cd9878e2 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Fri, 5 Nov 2021 09:05:58 +0900 Subject: [PATCH 12/32] Fix macos build errors. --- src/apple/cli.rs | 2 +- src/os/macos/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apple/cli.rs b/src/apple/cli.rs index 776e857a..6af2f49a 100644 --- a/src/apple/cli.rs +++ b/src/apple/cli.rs @@ -135,7 +135,7 @@ pub enum Error { MetadataFailed(metadata::Error), Unsupported, ProjectDirAbsent { project_dir: PathBuf }, - OpenFailed(bossy::Error), + OpenFailed(os::OpenFileError), CheckFailed(CheckError), BuildFailed(BuildError), ArchiveFailed(ArchiveError), diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs index 01c54eb0..53eec169 100644 --- a/src/os/macos/mod.rs +++ b/src/os/macos/mod.rs @@ -9,7 +9,7 @@ use core_foundation::{ url::CFURL, }; use std::{ - ffi::OsStr, + ffi::{OsStr, OsString}, fmt::{self, Display}, path::{Path, PathBuf}, ptr, From 13831f603ed9dd436788e08de72d8655b1546ca9 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Fri, 5 Nov 2021 20:15:31 +0900 Subject: [PATCH 13/32] Avoid Windows invalid UNC path. On Windows, Path::join may makes invalid UNC path. It was fixed on nightly (https://github.com/rust-lang/rust/pull/89270), but not yet on stable. --- src/util/path.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/util/path.rs b/src/util/path.rs index fff8a249..0022315a 100644 --- a/src/util/path.rs +++ b/src/util/path.rs @@ -2,7 +2,7 @@ use path_abs::PathAbs; use std::{ fmt::{self, Display}, io, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; use thiserror::Error; @@ -84,7 +84,33 @@ impl Display for PathNotPrefixed { } pub fn prefix_path(root: impl AsRef, path: impl AsRef) -> PathBuf { - root.as_ref().join(path) + let root = root.as_ref(); + let path = path.as_ref(); + let is_verbatim = if let Some(Component::Prefix(prefix)) = root.components().next() { + prefix.kind().is_verbatim() + } else { + false + }; + if !is_verbatim { + return root.join(path); + } + let mut buf = root.components().collect::>(); + for component in path.components() { + match component { + Component::RootDir => { + buf.truncate(1); + buf.push(component); + } + Component::CurDir => {} + Component::ParentDir => { + if let Some(_) = buf.last() { + buf.pop(); + } + } + _ => buf.push(component), + }; + } + buf.into_iter().collect() } pub fn unprefix_path( From 437f29e47b39949173e9fb9c6acd8f1d194516a9 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Fri, 5 Nov 2021 20:18:48 +0900 Subject: [PATCH 14/32] Link assets directory on build time --- src/android/project.rs | 8 ---- .../android-studio/app/build.gradle.kts.hbs | 22 +++------ .../buildSrc/src/main/kotlin/LinkTask.kt.hbs | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs diff --git a/src/android/project.rs b/src/android/project.rs index 0b7294b1..c87b9eff 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -12,7 +12,6 @@ use crate::{ util::{ self, cli::{Report, Reportable, TextWrapper}, - ln, }, }; use path_abs::PathOps; @@ -33,7 +32,6 @@ pub enum Error { path: PathBuf, cause: std::io::Error, }, - AssetDirSymlinkFailed(ln::Error), DotCargoGenFailed(ndk::MissingToolError), FileCopyFailed { src: PathBuf, @@ -55,9 +53,6 @@ impl Reportable for Error { format!("Failed to create Android assets directory at {:?}", path), cause, ), - Self::AssetDirSymlinkFailed(err) => { - Report::error("Asset dir couldn't be symlinked into Android project", err) - } Self::DotCargoGenFailed(err) => { Report::error("Failed to generate Android cargo config", err) } @@ -183,9 +178,6 @@ pub fn gen( path: dest.clone(), cause, })?; - #[cfg(not(windows))] - ln::force_symlink_relative(config.app().asset_dir(), dest, ln::TargetStyle::Directory) - .map_err(Error::AssetDirSymlinkFailed)?; { for target in Target::all().values() { diff --git a/templates/platforms/android-studio/app/build.gradle.kts.hbs b/templates/platforms/android-studio/app/build.gradle.kts.hbs index 8f2cc67c..1d8d2235 100644 --- a/templates/platforms/android-studio/app/build.gradle.kts.hbs +++ b/templates/platforms/android-studio/app/build.gradle.kts.hbs @@ -61,19 +61,10 @@ dependencies { implementation("{{this}}"){{/each}} } -{{#if windows}}tasks.register("syncAssets") { - val root = "{{root-dir-rel}}../assets" - val absoluteRoot = file(root).absoluteFile - into("src/main/assets") - fileTree(root).forEach() { file -> - if (file.isFile) { - val relativeDir = file.parentFile.toRelativeString(absoluteRoot) - from(file) { - into(relativeDir) - } - } - } -}{{/if}} +tasks.register<{{reverse-domain app.domain}}.LinkTask>("linkAssets") { + src = "{{root-dir-rel}}../assets" + dest = "src/main/assets" +} afterEvaluate { android.applicationVariants.all { @@ -81,9 +72,8 @@ afterEvaluate { productFlavors.forEach { val archAndBuildType = name.capitalize() tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"]) - {{#if windows}} - tasks["rustBuild${archAndBuildType}"].dependsOn(tasks["syncAssets"]) - tasks["merge${archAndBuildType}Assets"].dependsOn("syncAssets"){{/if}} + tasks["rustBuild${archAndBuildType}"].dependsOn(tasks["linkAssets"]) + tasks["merge${archAndBuildType}Assets"].dependsOn(tasks["linkAssets"]) } } } diff --git a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs new file mode 100644 index 00000000..9d7dd5b8 --- /dev/null +++ b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs @@ -0,0 +1,46 @@ +package {{reverse-domain app.domain}} + +import java.nio.file.FileSystemException +import java.nio.file.Files +import java.nio.file.Paths +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.tasks.* + +open class LinkTask : DefaultTask() { + @InputDirectory @PathSensitive(PathSensitivity.RELATIVE) var src: String? = null + @OutputDirectory var dest: String? = null + @TaskAction + fun link() { + val src = src ?: throw GradleException("src cannot be null") + val srcFile = project.file(src) + val destFile = project.file(dest ?: throw GradleException("dest cannot be null")) + val destPath = Paths.get(destFile.absolutePath) + if (destFile.isDirectory) { + destFile.deleteRecursively() + } + // Try creation soft link + try { + Files.createSymbolicLink(destPath, Paths.get(src)) + } catch (e: FileSystemException) { + project.fileTree(src).forEach { file -> + if (file.isFile) { + val relativePath = file.toRelativeString(srcFile) + val target = destFile.resolve(relativePath) + val parent = project.file(target.parent) + parent.mkdirs() + val targetPath = Paths.get(target.absolutePath) + val srcPath = Paths.get(file.absolutePath) + // Try creation hard link + try { + Files.createLink(targetPath, srcPath) + } catch (e: FileSystemException) { + // Copy file (fallback) + System.err.println(e) + Files.copy(srcPath, targetPath) + } + } + } + } + } +} From e366a81faf2611add46199b9c280ae6d04f69325 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Fri, 5 Nov 2021 21:35:29 +0900 Subject: [PATCH 15/32] Fix path errors. --- src/android/device.rs | 12 ++++++++---- src/android/jnilibs.rs | 9 +++++---- src/android/project.rs | 3 ++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/android/device.rs b/src/android/device.rs index 0626c067..6cf2b4b5 100644 --- a/src/android/device.rs +++ b/src/android/device.rs @@ -12,6 +12,7 @@ use crate::{ util::{ self, cli::{Report, Reportable}, + prefix_path, }, }; use std::{ @@ -180,10 +181,13 @@ impl<'a> Device<'a> { flavor: &str, ) -> PathBuf { let suffix = Self::suffix(profile); - config.project_dir().join(format!( - "app/build/outputs/{}/app-{}-{}.{}", - output_dir, flavor, suffix, file_extension - )) + prefix_path( + config.project_dir(), + format!( + "app/build/outputs/{}/app-{}-{}.{}", + output_dir, flavor, suffix, file_extension + ), + ) } fn apk_path(config: &Config, profile: Profile, flavor: &str) -> PathBuf { diff --git a/src/android/jnilibs.rs b/src/android/jnilibs.rs index c9d10993..4d6ba063 100644 --- a/src/android/jnilibs.rs +++ b/src/android/jnilibs.rs @@ -5,7 +5,7 @@ use crate::{ target::TargetTrait as _, util::{ cli::{Report, Reportable}, - ln, + ln, prefix_path, }, }; use std::path::{Path, PathBuf}; @@ -65,9 +65,10 @@ impl Reportable for SymlinkLibError { } pub fn path(config: &Config, target: Target<'_>) -> PathBuf { - config - .project_dir() - .join(format!("app/src/main/jniLibs/{}", &target.abi)) + prefix_path( + config.project_dir(), + format!("app/src/main/jniLibs/{}", &target.abi), + ) } #[derive(Debug)] diff --git a/src/android/project.rs b/src/android/project.rs index c87b9eff..338464f1 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -12,6 +12,7 @@ use crate::{ util::{ self, cli::{Report, Reportable, TextWrapper}, + prefix_path, }, }; use path_abs::PathOps; @@ -173,7 +174,7 @@ pub fn gen( })?; } - let dest = dest.join("app/src/main/assets/"); + let dest = prefix_path(dest, "app/src/main/assets/"); fs::create_dir_all(&dest).map_err(|cause| Error::DirectoryCreationFailed { path: dest.clone(), cause, From 469c7ce923fabc742abd0b71e1716ffc2c3bc06e Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 6 Nov 2021 09:01:51 +0900 Subject: [PATCH 16/32] Fix symlink target path mistake. --- .../android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs index 9d7dd5b8..3eae098a 100644 --- a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs +++ b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs @@ -16,12 +16,13 @@ open class LinkTask : DefaultTask() { val srcFile = project.file(src) val destFile = project.file(dest ?: throw GradleException("dest cannot be null")) val destPath = Paths.get(destFile.absolutePath) - if (destFile.isDirectory) { + if (destFile.exists()) { destFile.deleteRecursively() } // Try creation soft link try { - Files.createSymbolicLink(destPath, Paths.get(src)) + val target = srcFile.toRelativeString(destFile); + Files.createSymbolicLink(destPath, Paths.get(target)); } catch (e: FileSystemException) { project.fileTree(src).forEach { file -> if (file.isFile) { From 76f04bf4b7906c4a5b0b26ab2dea6e9bbd3b42d2 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 6 Nov 2021 13:03:38 +0900 Subject: [PATCH 17/32] Refix symlink path. --- .../android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs index 3eae098a..ebb14413 100644 --- a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs +++ b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs @@ -21,7 +21,7 @@ open class LinkTask : DefaultTask() { } // Try creation soft link try { - val target = srcFile.toRelativeString(destFile); + val target = srcFile.toRelativeString(destFile.parentFile); Files.createSymbolicLink(destPath, Paths.get(target)); } catch (e: FileSystemException) { project.fileTree(src).forEach { file -> From 6f4ae53fbcc4c4d09efec1494e256cabc1b43cbf Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Wed, 10 Nov 2021 01:52:43 +0900 Subject: [PATCH 18/32] Revert "Refix symlink path." This reverts commit 76f04bf4b7906c4a5b0b26ab2dea6e9bbd3b42d2. --- .../android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs index ebb14413..3eae098a 100644 --- a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs +++ b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs @@ -21,7 +21,7 @@ open class LinkTask : DefaultTask() { } // Try creation soft link try { - val target = srcFile.toRelativeString(destFile.parentFile); + val target = srcFile.toRelativeString(destFile); Files.createSymbolicLink(destPath, Paths.get(target)); } catch (e: FileSystemException) { project.fileTree(src).forEach { file -> From c06a750548a68d5a2a0057435b619d7004bfbb1c Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Wed, 10 Nov 2021 01:52:57 +0900 Subject: [PATCH 19/32] Revert "Fix symlink target path mistake." This reverts commit 469c7ce923fabc742abd0b71e1716ffc2c3bc06e. --- .../android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs index 3eae098a..9d7dd5b8 100644 --- a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs +++ b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs @@ -16,13 +16,12 @@ open class LinkTask : DefaultTask() { val srcFile = project.file(src) val destFile = project.file(dest ?: throw GradleException("dest cannot be null")) val destPath = Paths.get(destFile.absolutePath) - if (destFile.exists()) { + if (destFile.isDirectory) { destFile.deleteRecursively() } // Try creation soft link try { - val target = srcFile.toRelativeString(destFile); - Files.createSymbolicLink(destPath, Paths.get(target)); + Files.createSymbolicLink(destPath, Paths.get(src)) } catch (e: FileSystemException) { project.fileTree(src).forEach { file -> if (file.isFile) { From cafeca57a21d1d5e206c07fe494c0abdabf9e941 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Wed, 10 Nov 2021 01:55:25 +0900 Subject: [PATCH 20/32] Revert "Link assets directory on build time" This reverts commit 437f29e47b39949173e9fb9c6acd8f1d194516a9. --- src/android/project.rs | 9 +++- .../android-studio/app/build.gradle.kts.hbs | 22 ++++++--- .../buildSrc/src/main/kotlin/LinkTask.kt.hbs | 46 ------------------- 3 files changed, 24 insertions(+), 53 deletions(-) delete mode 100644 templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs diff --git a/src/android/project.rs b/src/android/project.rs index 338464f1..fd72e185 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -12,7 +12,7 @@ use crate::{ util::{ self, cli::{Report, Reportable, TextWrapper}, - prefix_path, + ln, prefix_path, }, }; use path_abs::PathOps; @@ -33,6 +33,7 @@ pub enum Error { path: PathBuf, cause: std::io::Error, }, + AssetDirSymlinkFailed(ln::Error), DotCargoGenFailed(ndk::MissingToolError), FileCopyFailed { src: PathBuf, @@ -54,6 +55,9 @@ impl Reportable for Error { format!("Failed to create Android assets directory at {:?}", path), cause, ), + Self::AssetDirSymlinkFailed(err) => { + Report::error("Asset dir couldn't be symlinked into Android project", err) + } Self::DotCargoGenFailed(err) => { Report::error("Failed to generate Android cargo config", err) } @@ -179,6 +183,9 @@ pub fn gen( path: dest.clone(), cause, })?; + #[cfg(not(windows))] + ln::force_symlink_relative(config.app().asset_dir(), dest, ln::TargetStyle::Directory) + .map_err(Error::AssetDirSymlinkFailed)?; { for target in Target::all().values() { diff --git a/templates/platforms/android-studio/app/build.gradle.kts.hbs b/templates/platforms/android-studio/app/build.gradle.kts.hbs index 1d8d2235..8f2cc67c 100644 --- a/templates/platforms/android-studio/app/build.gradle.kts.hbs +++ b/templates/platforms/android-studio/app/build.gradle.kts.hbs @@ -61,10 +61,19 @@ dependencies { implementation("{{this}}"){{/each}} } -tasks.register<{{reverse-domain app.domain}}.LinkTask>("linkAssets") { - src = "{{root-dir-rel}}../assets" - dest = "src/main/assets" -} +{{#if windows}}tasks.register("syncAssets") { + val root = "{{root-dir-rel}}../assets" + val absoluteRoot = file(root).absoluteFile + into("src/main/assets") + fileTree(root).forEach() { file -> + if (file.isFile) { + val relativeDir = file.parentFile.toRelativeString(absoluteRoot) + from(file) { + into(relativeDir) + } + } + } +}{{/if}} afterEvaluate { android.applicationVariants.all { @@ -72,8 +81,9 @@ afterEvaluate { productFlavors.forEach { val archAndBuildType = name.capitalize() tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"]) - tasks["rustBuild${archAndBuildType}"].dependsOn(tasks["linkAssets"]) - tasks["merge${archAndBuildType}Assets"].dependsOn(tasks["linkAssets"]) + {{#if windows}} + tasks["rustBuild${archAndBuildType}"].dependsOn(tasks["syncAssets"]) + tasks["merge${archAndBuildType}Assets"].dependsOn("syncAssets"){{/if}} } } } diff --git a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs b/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs deleted file mode 100644 index 9d7dd5b8..00000000 --- a/templates/platforms/android-studio/buildSrc/src/main/kotlin/LinkTask.kt.hbs +++ /dev/null @@ -1,46 +0,0 @@ -package {{reverse-domain app.domain}} - -import java.nio.file.FileSystemException -import java.nio.file.Files -import java.nio.file.Paths -import org.gradle.api.DefaultTask -import org.gradle.api.GradleException -import org.gradle.api.tasks.* - -open class LinkTask : DefaultTask() { - @InputDirectory @PathSensitive(PathSensitivity.RELATIVE) var src: String? = null - @OutputDirectory var dest: String? = null - @TaskAction - fun link() { - val src = src ?: throw GradleException("src cannot be null") - val srcFile = project.file(src) - val destFile = project.file(dest ?: throw GradleException("dest cannot be null")) - val destPath = Paths.get(destFile.absolutePath) - if (destFile.isDirectory) { - destFile.deleteRecursively() - } - // Try creation soft link - try { - Files.createSymbolicLink(destPath, Paths.get(src)) - } catch (e: FileSystemException) { - project.fileTree(src).forEach { file -> - if (file.isFile) { - val relativePath = file.toRelativeString(srcFile) - val target = destFile.resolve(relativePath) - val parent = project.file(target.parent) - parent.mkdirs() - val targetPath = Paths.get(target.absolutePath) - val srcPath = Paths.get(file.absolutePath) - // Try creation hard link - try { - Files.createLink(targetPath, srcPath) - } catch (e: FileSystemException) { - // Copy file (fallback) - System.err.println(e) - Files.copy(srcPath, targetPath) - } - } - } - } - } -} From 266f2d00891606936f2189f56541aac41b5b41a4 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 13 Nov 2021 15:56:43 +0900 Subject: [PATCH 21/32] Symlink for windows. --- Cargo.lock | 59 +++++------- Cargo.toml | 22 ++++- build.rs | 17 ---- src/android/jnilibs.rs | 9 +- src/android/project.rs | 5 +- src/os/linux/mod.rs | 2 + src/os/macos/mod.rs | 2 + src/os/windows/info.rs | 5 +- src/os/windows/ln.rs | 212 +++++++++++++++++++++++++++++++++-------- src/os/windows/mod.rs | 35 ++++--- 10 files changed, 242 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2ee4fda..c206f037 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,12 +262,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "const-sha1" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" - [[package]] name = "const-utf16" version = "0.2.1" @@ -1470,49 +1464,46 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.21.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f5f8d2ea79bf690bbee453fd4a1516ae426e5d5c7215d96cc0c3dc134fc4a0" +checksum = "347cdcaae1addebdff584aea1f9fbc0426dedbe1315f1dcf30c7a9876401cd25" dependencies = [ - "const-sha1", - "windows_gen", - "windows_macros", - "windows_reader", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", ] [[package]] -name = "windows_gen" -version = "0.21.1" +name = "windows_aarch64_msvc" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6994f42f8481387778cc608407d6703410672d57f32a66009419d7a18aa912" -dependencies = [ - "windows_quote", - "windows_reader", -] +checksum = "f7758986b022add546ae53ccad31f4852ce6bd2e2c2d3cc2b1d7d06dea0b90da" [[package]] -name = "windows_macros" -version = "0.21.1" +name = "windows_i686_gnu" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cc2357b1b03c19f056cb0e6d06011f80f54beadb4e36aee2ca98493c7cfc3c" -dependencies = [ - "syn", - "windows_gen", - "windows_quote", - "windows_reader", -] +checksum = "29261214caab8e589f61031ba1ccd5c3c25e61db2118a3aec4459f58ff798726" [[package]] -name = "windows_quote" -version = "0.21.1" +name = "windows_i686_msvc" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf987b5288c15e1997226848f78f3ed3ef8b78dcfd71a201c8c8684163a7e4d" +checksum = "43984fb3b944743142112ae926e7adeccb60f35bb81d43114f4d0fe2871f60ba" [[package]] -name = "windows_reader" -version = "0.21.1" +name = "windows_x86_64_gnu" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a80fc90e1ad19769e596a3f58d0776319059e21cac9069a5a2a791362ce7190" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237b53e8b40766ea7db5da0d8c6c1442d21d0429f0ee7500d7b5688967bd9d7b" +checksum = "dc24ddac19a0cf02ad2b32d8897f202fc1a13ef285e2d4774e6610783cc8398f" [[package]] name = "winreg" diff --git a/Cargo.toml b/Cargo.toml index 4f9e843d..9417c7d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,24 @@ lexical-core = "0.7.6" [target.'cfg(windows)'.dependencies] const-utf16 = "0.2.1" -windows = "0.21.1" + +[target.'cfg(windows)'.dependencies.windows] +version = "0.26.0" +features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Diagnostics_Debug", + "Win32_System_IO", + "Win32_System_Ioctl", + "Win32_System_Memory", + "Win32_System_Registry", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_UI_Shell", + "std", +] + [build-dependencies] bicycle = { git = "https://github.com/BrainiumLLC/bicycle", rev = "28080e0c6fa4067d9dd1b0f2b7322b6b32178e1f" } @@ -73,5 +90,4 @@ hit = "0.1.0" home = "0.5.3" [target.'cfg(windows)'.build-dependencies] -embed-resource = "1.6.4" -windows = "0.21.1" +embed-resource = "1.6.4" \ No newline at end of file diff --git a/build.rs b/build.rs index 007c4429..4395ccfe 100644 --- a/build.rs +++ b/build.rs @@ -66,22 +66,5 @@ fn main() { println!("cargo:rerun-if-changed={}", resource_path.display()); println!("cargo:rerun-if-changed={}", manifest_path.display()); embed_resource::compile("cargo-mobile-manifest.rc"); - - // Build winapi bindings - windows::build! { - Windows::Win32::{ - Foundation::MAX_PATH, - System::{ - Diagnostics::Debug::{GetLastError, WIN32_ERROR}, - Memory::LocalFree, - SystemInformation::{VerifyVersionInfoW, VerSetConditionMask}, - SystemServices::{VER_EQUAL, VER_GREATER_EQUAL}, - Registry::HKEY_LOCAL_MACHINE, - - }, - UI::Shell::{ASSOCF_INIT_IGNOREUNKNOWN, AssocQueryStringW, CommandLineToArgvW, SHRegGetPathW}, - Storage::FileSystem::CreateHardLinkW, - }, - } } } diff --git a/src/android/jnilibs.rs b/src/android/jnilibs.rs index 4d6ba063..a45b4fad 100644 --- a/src/android/jnilibs.rs +++ b/src/android/jnilibs.rs @@ -1,7 +1,6 @@ use super::{config::Config, target::Target}; -#[cfg(windows)] -use crate::os; use crate::{ + os, target::TargetTrait as _, util::{ cli::{Report, Reportable}, @@ -125,11 +124,7 @@ impl JniLibs { src.file_name() .expect("developer error: file had no file name"), ); - #[cfg(not(windows))] - ln::force_symlink(src, &dest, ln::TargetStyle::File) - .map_err(SymlinkLibError::SymlinkFailed)?; - #[cfg(windows)] - os::ln::force_hard_link_or_copy_file(src, &dest) + os::ln::force_symlink(src, &dest, ln::TargetStyle::File) .map_err(SymlinkLibError::SymlinkFailed)?; Ok(()) } else { diff --git a/src/android/project.rs b/src/android/project.rs index fd72e185..113f1b5f 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -6,7 +6,7 @@ use super::{ }; use crate::{ dot_cargo, - os::replace_path_separator, + os::{self, replace_path_separator}, target::TargetTrait as _, templating::{self, Pack}, util::{ @@ -183,8 +183,7 @@ pub fn gen( path: dest.clone(), cause, })?; - #[cfg(not(windows))] - ln::force_symlink_relative(config.app().asset_dir(), dest, ln::TargetStyle::Directory) + os::ln::force_symlink_relative(config.app().asset_dir(), dest, ln::TargetStyle::Directory) .map_err(Error::AssetDirSymlinkFailed)?; { diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs index 50cd315e..6fc79991 100644 --- a/src/os/linux/mod.rs +++ b/src/os/linux/mod.rs @@ -8,6 +8,8 @@ use std::{ path::{Path, PathBuf}, }; +pub use crate::util::ln; + #[derive(Debug)] pub enum DetectEditorError { NoDefaultEditorSet, diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs index 53eec169..6bac7f80 100644 --- a/src/os/macos/mod.rs +++ b/src/os/macos/mod.rs @@ -15,6 +15,8 @@ use std::{ ptr, }; +pub use crate::util::ln; + // This can hopefully be relied upon... https://stackoverflow.com/q/8003919 static RUST_UTI: &str = "dyn.ah62d4rv4ge81e62"; diff --git a/src/os/windows/info.rs b/src/os/windows/info.rs index 32a9ad2c..92219321 100644 --- a/src/os/windows/info.rs +++ b/src/os/windows/info.rs @@ -1,12 +1,13 @@ -use super::Windows::Win32::System::{ +use thiserror::Error; +use windows::Win32::System::{ SystemInformation::{ VerSetConditionMask, VerifyVersionInfoW, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_MAJORVERSION, VER_MINORVERSION, VER_PRODUCT_TYPE, VER_SERVICEPACKMAJOR, }, SystemServices::{VER_EQUAL, VER_GREATER_EQUAL}, }; + use crate::os::Info; -use thiserror::Error; #[derive(Debug, Error)] pub enum Error { diff --git a/src/os/windows/ln.rs b/src/os/windows/ln.rs index 8f17f446..9d39b3b2 100644 --- a/src/os/windows/ln.rs +++ b/src/os/windows/ln.rs @@ -1,50 +1,180 @@ -use super::Windows::Win32::{Foundation::PWSTR, Storage::FileSystem::CreateHardLinkW}; use crate::util::ln::{Clobber, Error, ErrorCause, LinkType, TargetStyle}; -use std::{os::windows::ffi::OsStrExt, path::Path}; +use std::{borrow::Cow, fs::remove_file, os::windows::ffi::OsStrExt, path::Path}; +use windows::{ + runtime, + Win32::{ + Foundation::{ + CloseHandle, GetLastError, BOOLEAN, ERROR_MORE_DATA, HANDLE, INVALID_HANDLE_VALUE, + PWSTR, + }, + Storage::FileSystem::{ + CreateFileW, CreateSymbolicLinkW, GetFileAttributesW, FILE_ACCESS_FLAGS, + FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_DELETE_ON_CLOSE, + FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, OPEN_EXISTING, REPARSE_GUID_DATA_BUFFER, + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, + }, + System::{ + Ioctl::FSCTL_GET_REPARSE_POINT, + SystemServices::{GENERIC_READ, IO_REPARSE_TAG_SYMLINK}, + IO::DeviceIoControl, + }, + }, +}; -pub fn force_hard_link_or_copy_file( - src: impl AsRef, - dest: impl AsRef, +pub fn force_symlink( + source: impl AsRef, + target: impl AsRef, + target_style: TargetStyle, ) -> Result<(), Error> { - let src = src.as_ref(); - let dest = dest.as_ref(); - let mut src_buf = src.as_os_str().encode_wide().collect::>(); - let mut dest_buf = dest.as_os_str().encode_wide().collect::>(); - src_buf.push(0); - dest_buf.push(0); - if dest.is_file() { - std::fs::remove_file(dest).map_err(|err| { - Error::new( - LinkType::Hard, - Clobber::FileOnly, - src.to_owned(), - dest.to_owned(), - TargetStyle::File, - ErrorCause::IOError(err), - ) - })?; - } - if !unsafe { - CreateHardLinkW( - PWSTR(dest_buf.as_mut_ptr()), - PWSTR(src_buf.as_mut_ptr()), - std::ptr::null_mut(), + let (source, target) = (source.as_ref(), target.as_ref()); + let error = |cause: ErrorCause| { + Error::new( + LinkType::Symbolic, + Clobber::FileOnly, + source.to_owned(), + target.to_owned(), + target_style, + cause, ) + }; + let target = if target_style == TargetStyle::Directory { + let file_name = if let Some(file_name) = source.file_name() { + file_name + } else { + return Err(error(ErrorCause::MissingFileName)); + }; + Cow::Owned(target.join(file_name)) + } else { + Cow::Borrowed(target) + }; + let target_wtf16 = target + .as_os_str() + .encode_wide() + .chain([0]) + .collect::>(); + if is_symlink(&target_wtf16) { + delete_symlink(&target_wtf16).map_err(|err| error(ErrorCause::IOError(err.into())))?; + } else if target.is_file() { + remove_file(&target).map_err(|err| error(ErrorCause::IOError(err)))?; } - .as_bool() - { - // If the drive is different between src and dest, the creation of the hard link will fail. - // In that case, fall back to the simple copy. - std::fs::copy(src, dest).map_err(|err| { - Error::new( - LinkType::Hard, + let source_wtf16 = source + .as_os_str() + .encode_wide() + .chain([0]) + .collect::>(); + create_symlink(&target_wtf16, &source_wtf16) + .map_err(|err| error(ErrorCause::IOError(err.into())))?; + Ok(()) +} + +pub fn force_symlink_relative( + abs_source: impl AsRef, + abs_target: impl AsRef, + target_style: TargetStyle, +) -> Result<(), Error> { + let (abs_source, abs_target) = (abs_source.as_ref(), abs_target.as_ref()); + let rel_source = crate::util::relativize_path(abs_source, abs_target); + if target_style == TargetStyle::Directory && rel_source.file_name().is_none() { + if let Some(file_name) = abs_source.file_name() { + force_symlink(rel_source, abs_target.join(file_name), TargetStyle::File) + } else { + Err(Error::new( + LinkType::Symbolic, Clobber::FileOnly, - src.to_owned(), - dest.to_owned(), - TargetStyle::File, - ErrorCause::IOError(err), - ) - })?; + rel_source, + abs_target.to_owned(), + target_style, + ErrorCause::MissingFileName, + )) + } + } else { + force_symlink(rel_source, abs_target, target_style) + } +} + +fn create_symlink(target: &[u16], source: &[u16]) -> Result<(), runtime::Error> { + if unsafe { + CreateSymbolicLinkW( + PWSTR(target.as_ptr() as _), + PWSTR(source.as_ptr() as _), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, + ) + } == BOOLEAN(0) + { + return Err(runtime::Error::from_win32().into()); + } + Ok(()) +} + +fn delete_symlink(filename: &[u16]) -> Result<(), runtime::Error> { + let handle = FileHandle(unsafe { + CreateFileW( + PWSTR(filename.as_ptr() as _), + FILE_ACCESS_FLAGS(GENERIC_READ), + FILE_SHARE_READ, + std::ptr::null(), + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_DELETE_ON_CLOSE, + HANDLE(0), + ) + }); + if handle.is_invalid() { + return Err(runtime::Error::from_win32()); } Ok(()) } + +fn is_symlink(filename: &[u16]) -> bool { + let attr = unsafe { GetFileAttributesW(PWSTR(filename.as_ptr() as _)) }; + if attr & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 { + return false; + } + let h_file = FileHandle(unsafe { + CreateFileW( + PWSTR(filename.as_ptr() as _), + FILE_ACCESS_FLAGS(GENERIC_READ), + FILE_SHARE_READ, + std::ptr::null(), + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + HANDLE(0), + ) + }); + if h_file.0 == INVALID_HANDLE_VALUE { + return false; + } + let mut buffer: REPARSE_GUID_DATA_BUFFER = unsafe { std::mem::zeroed() }; + let mut bytes = 0u32; + let result = unsafe { + DeviceIoControl( + h_file.0, + FSCTL_GET_REPARSE_POINT, + std::ptr::null(), + 0, + &mut buffer as *mut _ as _, + std::mem::size_of::() as _, + (&mut bytes) as _, + std::ptr::null_mut(), + ) + }; + if !result.as_bool() && unsafe { GetLastError() } != ERROR_MORE_DATA { + return false; + } + return buffer.ReparseTag as i32 == IO_REPARSE_TAG_SYMLINK; +} + +struct FileHandle(HANDLE); + +impl FileHandle { + fn is_invalid(&self) -> bool { + self.0 == INVALID_HANDLE_VALUE + } +} + +impl Drop for FileHandle { + fn drop(&mut self) { + if !self.is_invalid() { + unsafe { CloseHandle(self.0) }; + } + } +} diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs index bc59e0f8..e745ed1f 100644 --- a/src/os/windows/mod.rs +++ b/src/os/windows/mod.rs @@ -8,18 +8,15 @@ use std::{ slice::from_raw_parts, }; use thiserror::Error; - -windows::include_bindings!(); - -use Windows::Win32::{ - Foundation::{MAX_PATH, PWSTR}, - System::{ - Diagnostics::Debug::ERROR_NO_ASSOCIATION, Diagnostics::Debug::ERROR_SUCCESS, - Memory::LocalFree, Registry::HKEY_LOCAL_MACHINE, - }, - UI::Shell::{ - AssocQueryStringW, CommandLineToArgvW, SHRegGetPathW, ASSOCF_INIT_IGNOREUNKNOWN, - ASSOCSTR_COMMAND, +use windows::{ + runtime, + Win32::{ + Foundation::{ERROR_NO_ASSOCIATION, ERROR_SUCCESS, MAX_PATH, PWSTR}, + System::{Memory::LocalFree, Registry::HKEY_LOCAL_MACHINE}, + UI::Shell::{ + AssocQueryStringW, CommandLineToArgvW, SHRegGetPathW, ASSOCF_INIT_IGNOREUNKNOWN, + ASSOCSTR_COMMAND, + }, }, }; @@ -28,12 +25,12 @@ pub enum DetectEditorError { #[error("No default editor is set: AssocQueryStringW for \".rs\" and \".txt\" both failed")] NoDefaultEditorSet, #[error("An error occured while calling AssocQueryStringW: {0}")] - OSError(#[source] windows::Error), + IOError(#[source] std::io::Error), } -impl From for DetectEditorError { - fn from(err: windows::Error) -> Self { - Self::OSError(err) +impl From for DetectEditorError { + fn from(err: runtime::Error) -> Self { + Self::IOError(err.into()) } } @@ -42,7 +39,7 @@ pub enum OpenFileError { #[error("Launch Failed: {0}")] LaunchFailed(#[source] bossy::Error), #[error("An error occured while calling OS API: {0}")] - OSError(#[source] windows::Error), + IOError(#[source] std::io::Error), } pub struct Application { @@ -91,7 +88,7 @@ impl Application { if e.code().0 == 0x80070000 | ERROR_NO_ASSOCIATION.0 { return Err(DetectEditorError::NoDefaultEditorSet); } - return Err(DetectEditorError::OSError(e)); + return Err(DetectEditorError::IOError(e.into())); } let mut command: Vec = vec![0; len as usize]; unsafe { @@ -177,7 +174,7 @@ fn open_file_with_android_studio(path: impl AsRef) -> Result<(), OpenFile ) }; if lstatus.0 as u32 != ERROR_SUCCESS.0 { - return Err(OpenFileError::OSError(windows::Error::from_win32())); + return Err(OpenFileError::IOError(runtime::Error::from_win32().into())); } let len = NullTerminatedWTF16Iterator(buffer.as_ptr()).count(); let uninstaller_path = OsString::from_wide(&buffer[..len]); From d0dab097736706ef320a56ae1889291f14a973b2 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 13 Nov 2021 17:48:16 +0900 Subject: [PATCH 22/32] Add error message for unpriviledged Windows. --- src/os/windows/ln.rs | 37 +++++++++++++++++++++++++++++-------- src/util/ln.rs | 16 ++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/os/windows/ln.rs b/src/os/windows/ln.rs index 9d39b3b2..d9c929c6 100644 --- a/src/os/windows/ln.rs +++ b/src/os/windows/ln.rs @@ -1,17 +1,20 @@ -use crate::util::ln::{Clobber, Error, ErrorCause, LinkType, TargetStyle}; +use crate::util::{ + ln::{Clobber, Error, ErrorCause, LinkType, TargetStyle}, + prefix_path, +}; use std::{borrow::Cow, fs::remove_file, os::windows::ffi::OsStrExt, path::Path}; use windows::{ runtime, Win32::{ Foundation::{ - CloseHandle, GetLastError, BOOLEAN, ERROR_MORE_DATA, HANDLE, INVALID_HANDLE_VALUE, - PWSTR, + CloseHandle, GetLastError, BOOLEAN, ERROR_MORE_DATA, ERROR_PRIVILEGE_NOT_HELD, HANDLE, + INVALID_HANDLE_VALUE, PWSTR, }, Storage::FileSystem::{ CreateFileW, CreateSymbolicLinkW, GetFileAttributesW, FILE_ACCESS_FLAGS, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, OPEN_EXISTING, REPARSE_GUID_DATA_BUFFER, - SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, SYMBOLIC_LINK_FLAG_DIRECTORY, }, System::{ Ioctl::FSCTL_GET_REPARSE_POINT, @@ -47,6 +50,10 @@ pub fn force_symlink( } else { Cow::Borrowed(target) }; + let is_directory = target + .parent() + .map(|parent| prefix_path(parent, source).is_dir()) + .unwrap_or(false); let target_wtf16 = target .as_os_str() .encode_wide() @@ -62,8 +69,13 @@ pub fn force_symlink( .encode_wide() .chain([0]) .collect::>(); - create_symlink(&target_wtf16, &source_wtf16) - .map_err(|err| error(ErrorCause::IOError(err.into())))?; + create_symlink(&source_wtf16, &target_wtf16, is_directory).map_err(|err| { + if err.win32_error() == Some(ERROR_PRIVILEGE_NOT_HELD.0) { + error(ErrorCause::SymlinkNotAllowed) + } else { + error(ErrorCause::IOError(err.into())) + } + })?; Ok(()) } @@ -92,12 +104,21 @@ pub fn force_symlink_relative( } } -fn create_symlink(target: &[u16], source: &[u16]) -> Result<(), runtime::Error> { +fn create_symlink( + source: &[u16], + target: &[u16], + is_directory: bool, +) -> Result<(), runtime::Error> { + let flags = if is_directory { + SYMBOLIC_LINK_FLAG_DIRECTORY + } else { + Default::default() + } | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; if unsafe { CreateSymbolicLinkW( PWSTR(target.as_ptr() as _), PWSTR(source.as_ptr() as _), - SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, + flags, ) } == BOOLEAN(0) { diff --git a/src/util/ln.rs b/src/util/ln.rs index bdeebc80..8319bf29 100644 --- a/src/util/ln.rs +++ b/src/util/ln.rs @@ -55,6 +55,7 @@ pub enum ErrorCause { MissingFileName, CommandFailed(bossy::Error), IOError(std::io::Error), + SymlinkNotAllowed, } impl Display for ErrorCause { @@ -65,6 +66,21 @@ impl Display for ErrorCause { } Self::CommandFailed(err) => write!(f, "`ln` command failed: {}", err), Self::IOError(err) => write!(f, "IO error: {}", err), + Self::SymlinkNotAllowed => { + write!( + f, + r" +Creation symbolic link is not allowed for this system. + +For Windows 10 or newer: +You should use developer mode. +See https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development + +For Window 8.1 or older: +You need `SeCreateSymbolicLinkPrivilege` security policy. +See https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links" + ) + } } } } From 1280bd34c9286a093615a353dbe7c2bce9ac1ade Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 13 Nov 2021 17:57:02 +0900 Subject: [PATCH 23/32] Add missing trailing newline in Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9417c7d1..69f24c4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,4 +90,4 @@ hit = "0.1.0" home = "0.5.3" [target.'cfg(windows)'.build-dependencies] -embed-resource = "1.6.4" \ No newline at end of file +embed-resource = "1.6.4" From 7037e0c6c4125537912a5aaf00a25367699624cf Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sat, 13 Nov 2021 18:01:12 +0900 Subject: [PATCH 24/32] Remove syncAssets task. --- .../android-studio/app/build.gradle.kts.hbs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/templates/platforms/android-studio/app/build.gradle.kts.hbs b/templates/platforms/android-studio/app/build.gradle.kts.hbs index 8f2cc67c..dde026cf 100644 --- a/templates/platforms/android-studio/app/build.gradle.kts.hbs +++ b/templates/platforms/android-studio/app/build.gradle.kts.hbs @@ -61,29 +61,12 @@ dependencies { implementation("{{this}}"){{/each}} } -{{#if windows}}tasks.register("syncAssets") { - val root = "{{root-dir-rel}}../assets" - val absoluteRoot = file(root).absoluteFile - into("src/main/assets") - fileTree(root).forEach() { file -> - if (file.isFile) { - val relativeDir = file.parentFile.toRelativeString(absoluteRoot) - from(file) { - into(relativeDir) - } - } - } -}{{/if}} - afterEvaluate { android.applicationVariants.all { val buildType = "${buildType.name.capitalize()}" productFlavors.forEach { val archAndBuildType = name.capitalize() tasks["merge${archAndBuildType}JniLibFolders"].dependsOn(tasks["rustBuild${archAndBuildType}"]) - {{#if windows}} - tasks["rustBuild${archAndBuildType}"].dependsOn(tasks["syncAssets"]) - tasks["merge${archAndBuildType}Assets"].dependsOn("syncAssets"){{/if}} } } } From 10d7cc8c8b312c42da062221644222a34708b643 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sun, 14 Nov 2021 09:27:20 +0900 Subject: [PATCH 25/32] Remove one level of redundant `assets` --- src/android/project.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/project.rs b/src/android/project.rs index 113f1b5f..c5130895 100644 --- a/src/android/project.rs +++ b/src/android/project.rs @@ -178,7 +178,7 @@ pub fn gen( })?; } - let dest = prefix_path(dest, "app/src/main/assets/"); + let dest = prefix_path(dest, "app/src/main/"); fs::create_dir_all(&dest).map_err(|cause| Error::DirectoryCreationFailed { path: dest.clone(), cause, From fa4862a50bf1a81787c7dea462a16f20838c31be Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sun, 14 Nov 2021 20:24:53 +0900 Subject: [PATCH 26/32] Refactoring a little --- src/os/windows/ln.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/os/windows/ln.rs b/src/os/windows/ln.rs index d9c929c6..dbc96445 100644 --- a/src/os/windows/ln.rs +++ b/src/os/windows/ln.rs @@ -4,11 +4,11 @@ use crate::util::{ }; use std::{borrow::Cow, fs::remove_file, os::windows::ffi::OsStrExt, path::Path}; use windows::{ - runtime, + runtime::{self, Handle as _}, Win32::{ Foundation::{ CloseHandle, GetLastError, BOOLEAN, ERROR_MORE_DATA, ERROR_PRIVILEGE_NOT_HELD, HANDLE, - INVALID_HANDLE_VALUE, PWSTR, + PWSTR, }, Storage::FileSystem::{ CreateFileW, CreateSymbolicLinkW, GetFileAttributesW, FILE_ACCESS_FLAGS, @@ -114,14 +114,14 @@ fn create_symlink( } else { Default::default() } | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; - if unsafe { + let result = unsafe { CreateSymbolicLinkW( PWSTR(target.as_ptr() as _), PWSTR(source.as_ptr() as _), flags, ) - } == BOOLEAN(0) - { + }; + if result == BOOLEAN(0) { return Err(runtime::Error::from_win32().into()); } Ok(()) @@ -158,13 +158,13 @@ fn is_symlink(filename: &[u16]) -> bool { std::ptr::null(), OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, - HANDLE(0), + HANDLE::default(), ) }); - if h_file.0 == INVALID_HANDLE_VALUE { + if h_file.is_invalid() { return false; } - let mut buffer: REPARSE_GUID_DATA_BUFFER = unsafe { std::mem::zeroed() }; + let mut buffer = REPARSE_GUID_DATA_BUFFER::default(); let mut bytes = 0u32; let result = unsafe { DeviceIoControl( @@ -188,7 +188,7 @@ struct FileHandle(HANDLE); impl FileHandle { fn is_invalid(&self) -> bool { - self.0 == INVALID_HANDLE_VALUE + self.0.is_invalid() } } From 879f42fee5794a2eb00f51a6b5b8e9952d8db825 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Mon, 15 Nov 2021 00:20:18 +0900 Subject: [PATCH 27/32] Use Clobber::FileOrDirectory instead of Clobber::File --- src/os/windows/ln.rs | 9 ++++++- src/util/ln.rs | 61 +++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/os/windows/ln.rs b/src/os/windows/ln.rs index dbc96445..e4732131 100644 --- a/src/os/windows/ln.rs +++ b/src/os/windows/ln.rs @@ -2,7 +2,12 @@ use crate::util::{ ln::{Clobber, Error, ErrorCause, LinkType, TargetStyle}, prefix_path, }; -use std::{borrow::Cow, fs::remove_file, os::windows::ffi::OsStrExt, path::Path}; +use std::{ + borrow::Cow, + fs::{remove_dir_all, remove_file}, + os::windows::ffi::OsStrExt, + path::Path, +}; use windows::{ runtime::{self, Handle as _}, Win32::{ @@ -63,6 +68,8 @@ pub fn force_symlink( delete_symlink(&target_wtf16).map_err(|err| error(ErrorCause::IOError(err.into())))?; } else if target.is_file() { remove_file(&target).map_err(|err| error(ErrorCause::IOError(err)))?; + } else if target.is_dir() { + remove_dir_all(&target).map_err(|err| error(ErrorCause::IOError(err)))?; } let source_wtf16 = source .as_os_str() diff --git a/src/util/ln.rs b/src/util/ln.rs index 8319bf29..99fa343f 100644 --- a/src/util/ln.rs +++ b/src/util/ln.rs @@ -1,5 +1,7 @@ use std::{ + borrow::Cow, fmt::{self, Display}, + fs::remove_dir_all, path::{Path, PathBuf}, }; @@ -130,7 +132,7 @@ pub struct Call<'a> { link_type: LinkType, force: Clobber, source: &'a Path, - target: &'a Path, + target: Cow<'a, Path>, target_style: TargetStyle, } @@ -142,10 +144,12 @@ impl<'a> Call<'a> { target: &'a Path, target_style: TargetStyle, ) -> Result { - if let TargetStyle::Directory = target_style { + let target = if let TargetStyle::Directory = target_style { // If the target is a directory, then the link name has to come from // the last component of the source. - if source.file_name().is_none() { + if let Some(file_name) = source.file_name() { + Cow::Owned(target.join(file_name)) + } else { return Err(Error { link_type, force, @@ -155,7 +159,9 @@ impl<'a> Call<'a> { cause: ErrorCause::MissingFileName, }); } - } + } else { + Cow::Borrowed(target) + }; Ok(Self { link_type, force, @@ -178,40 +184,31 @@ impl<'a> Call<'a> { command.add_arg("-f"); } Clobber::FileOrDirectory => { - command.add_arg("-F"); + if self.target.is_dir() { + remove_dir_all(&self.target) + .map_err(|err| self.make_error(ErrorCause::IOError(err)))?; + } + command.add_arg("-f"); } _ => (), } - // For the target to be interpreted as a directory, it must end in a - // trailing slash. We can't append one using `join` or `push`, since it - // would be interpreted as an absolute path and result in the target - // being replaced with it: https://github.com/rust-lang/rust/issues/16507 - let target_override = if self.target_style == TargetStyle::Directory - && (!self.target.ends_with("/") || self.target.as_os_str().is_empty()) - { - Some(format!("{}/", self.target.display())) - } else { - None - }; command.add_arg(self.source); - if let Some(target) = target_override.as_ref() { - command.add_arg(target); - } else { - command.add_arg(self.target); - } - command.run_and_wait().map_err(|err| Error { + command.add_arg(self.target.as_ref()); + command + .run_and_wait() + .map_err(|err| self.make_error(ErrorCause::CommandFailed(err)))?; + Ok(()) + } + + fn make_error(&self, cause: ErrorCause) -> Error { + Error { link_type: self.link_type, force: self.force, source: self.source.to_owned(), - target: if let Some(target) = target_override { - target.into() - } else { - self.target.to_owned() - }, + target: self.target.to_path_buf(), target_style: self.target_style, - cause: ErrorCause::CommandFailed(err), - })?; - Ok(()) + cause, + } } } @@ -222,7 +219,7 @@ pub fn force_symlink( ) -> Result<(), Error> { Call::new( LinkType::Symbolic, - Clobber::FileOnly, + Clobber::FileOrDirectory, source.as_ref(), target.as_ref(), target_style, @@ -243,7 +240,7 @@ pub fn force_symlink_relative( } else { Err(Error { link_type: LinkType::Symbolic, - force: Clobber::FileOnly, + force: Clobber::FileOrDirectory, source: rel_source, target: abs_target.to_owned(), target_style, From a64450ad23abe19ec7dd9da1f5582960c607d02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Tue, 16 Nov 2021 00:51:04 +0100 Subject: [PATCH 28/32] add simple prefix_path tests --- src/util/path.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/util/path.rs b/src/util/path.rs index 0022315a..62112697 100644 --- a/src/util/path.rs +++ b/src/util/path.rs @@ -218,3 +218,43 @@ pub fn under_root( ) -> Result { normalize_path(root.as_ref().join(path)).map(|norm| norm.starts_with(root)) } + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest(root, path, result, + // UNIX + #[cfg(any(target_os = "linux", target_os = "macos"))] + case( + "/home/user/cargo-mobile-project/gen/android/cargo-mobile-project", + "app/build/outputs/apk/arm64/debug/app-arm64-debug.apk", + "/home/user/cargo-mobile-project/gen/android/cargo-mobile-project/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk" + ), + // UNIX but the second path contains root + #[cfg(any(target_os = "linux", target_os = "macos"))] + case( + "/home/user/cargo-mobile-project/gen/android/cargo-mobile-project", + "/home/other/project/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk", + "/home/other/project/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk" + ), + // Windows UNC + #[cfg(windows)] + case( + "\\\\?\\C:\\Users\\user\\cargo-mobile-project\\gen\\android\\cargo-mobile-project", + "app\\..\\app\\build\\outputs\\.\\apk\\arm64\\debug\\app-arm64-debug.apk", + "\\\\?\\C:\\Users\\user\\cargo-mobile-project\\gen\\android\\cargo-mobile-project\\app\\build\\outputs\\apk\\arm64\\debug\\app-arm64-debug.apk" + ), + // Windows legacy + #[cfg(windows)] + case ( + "D:\\Users\\user\\cargo-mobile-project\\gen\\android\\cargo-mobile-project", + "app\\build\\outputs\\apk\\arm64\\debug\\app-arm64-debug.apk", + "D:\\Users\\user\\cargo-mobile-project\\gen\\android\\cargo-mobile-project\\app\\build\\outputs\\apk\\arm64\\debug\\app-arm64-debug.apk" + ) + )] + fn test_prefix_path(root: impl AsRef, path: impl AsRef, result: &str) { + assert_eq!(prefix_path(root, path), PathBuf::from(result)); + } +} From 0cd4a6a203b3a30b2a93d732269d0c12f291bae1 Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sun, 28 Nov 2021 16:31:22 +0900 Subject: [PATCH 29/32] Use original target path in error message. --- src/util/ln.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/util/ln.rs b/src/util/ln.rs index 99fa343f..0e48a57e 100644 --- a/src/util/ln.rs +++ b/src/util/ln.rs @@ -132,7 +132,8 @@ pub struct Call<'a> { link_type: LinkType, force: Clobber, source: &'a Path, - target: Cow<'a, Path>, + target: &'a Path, + target_override: Cow<'a, Path>, target_style: TargetStyle, } @@ -144,7 +145,7 @@ impl<'a> Call<'a> { target: &'a Path, target_style: TargetStyle, ) -> Result { - let target = if let TargetStyle::Directory = target_style { + let target_override = if let TargetStyle::Directory = target_style { // If the target is a directory, then the link name has to come from // the last component of the source. if let Some(file_name) = source.file_name() { @@ -167,6 +168,7 @@ impl<'a> Call<'a> { force, source, target, + target_override, target_style, }) } @@ -184,7 +186,7 @@ impl<'a> Call<'a> { command.add_arg("-f"); } Clobber::FileOrDirectory => { - if self.target.is_dir() { + if self.target_override.is_dir() { remove_dir_all(&self.target) .map_err(|err| self.make_error(ErrorCause::IOError(err)))?; } @@ -193,7 +195,7 @@ impl<'a> Call<'a> { _ => (), } command.add_arg(self.source); - command.add_arg(self.target.as_ref()); + command.add_arg(self.target_override.as_ref()); command .run_and_wait() .map_err(|err| self.make_error(ErrorCause::CommandFailed(err)))?; @@ -205,7 +207,7 @@ impl<'a> Call<'a> { link_type: self.link_type, force: self.force, source: self.source.to_owned(), - target: self.target.to_path_buf(), + target: self.target.to_owned(), target_style: self.target_style, cause, } From 2a1de52ee4ff2362f15d1c59e2d1c30b5241c36a Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sun, 28 Nov 2021 16:34:30 +0900 Subject: [PATCH 30/32] Use std symlink function instead of WinAPI --- src/os/windows/ln.rs | 113 ++++++++++--------------------------------- 1 file changed, 25 insertions(+), 88 deletions(-) diff --git a/src/os/windows/ln.rs b/src/os/windows/ln.rs index e4732131..ddd1efba 100644 --- a/src/os/windows/ln.rs +++ b/src/os/windows/ln.rs @@ -11,21 +11,12 @@ use std::{ use windows::{ runtime::{self, Handle as _}, Win32::{ - Foundation::{ - CloseHandle, GetLastError, BOOLEAN, ERROR_MORE_DATA, ERROR_PRIVILEGE_NOT_HELD, HANDLE, - PWSTR, - }, + Foundation::{CloseHandle, ERROR_PRIVILEGE_NOT_HELD, HANDLE, PWSTR}, Storage::FileSystem::{ - CreateFileW, CreateSymbolicLinkW, GetFileAttributesW, FILE_ACCESS_FLAGS, - FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_DELETE_ON_CLOSE, - FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, OPEN_EXISTING, REPARSE_GUID_DATA_BUFFER, - SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, SYMBOLIC_LINK_FLAG_DIRECTORY, - }, - System::{ - Ioctl::FSCTL_GET_REPARSE_POINT, - SystemServices::{GENERIC_READ, IO_REPARSE_TAG_SYMLINK}, - IO::DeviceIoControl, + CreateFileW, FILE_ACCESS_FLAGS, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_DELETE_ON_CLOSE, + FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, OPEN_EXISTING, }, + System::SystemServices::GENERIC_READ, }, }; @@ -59,28 +50,23 @@ pub fn force_symlink( .parent() .map(|parent| prefix_path(parent, source).is_dir()) .unwrap_or(false); - let target_wtf16 = target - .as_os_str() - .encode_wide() - .chain([0]) - .collect::>(); - if is_symlink(&target_wtf16) { - delete_symlink(&target_wtf16).map_err(|err| error(ErrorCause::IOError(err.into())))?; + if is_symlink(&target) { + delete_symlink(&target).map_err(|err| error(ErrorCause::IOError(err.into())))?; } else if target.is_file() { remove_file(&target).map_err(|err| error(ErrorCause::IOError(err)))?; } else if target.is_dir() { remove_dir_all(&target).map_err(|err| error(ErrorCause::IOError(err)))?; } - let source_wtf16 = source - .as_os_str() - .encode_wide() - .chain([0]) - .collect::>(); - create_symlink(&source_wtf16, &target_wtf16, is_directory).map_err(|err| { - if err.win32_error() == Some(ERROR_PRIVILEGE_NOT_HELD.0) { + let result = if is_directory { + std::os::windows::fs::symlink_dir(source, target) + } else { + std::os::windows::fs::symlink_file(source, target) + }; + result.map_err(|err| { + if err.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD.0 as i32) { error(ErrorCause::SymlinkNotAllowed) } else { - error(ErrorCause::IOError(err.into())) + error(ErrorCause::IOError(err)) } })?; Ok(()) @@ -111,30 +97,12 @@ pub fn force_symlink_relative( } } -fn create_symlink( - source: &[u16], - target: &[u16], - is_directory: bool, -) -> Result<(), runtime::Error> { - let flags = if is_directory { - SYMBOLIC_LINK_FLAG_DIRECTORY - } else { - Default::default() - } | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; - let result = unsafe { - CreateSymbolicLinkW( - PWSTR(target.as_ptr() as _), - PWSTR(source.as_ptr() as _), - flags, - ) - }; - if result == BOOLEAN(0) { - return Err(runtime::Error::from_win32().into()); - } - Ok(()) -} - -fn delete_symlink(filename: &[u16]) -> Result<(), runtime::Error> { +fn delete_symlink(filename: &Path) -> Result<(), runtime::Error> { + let filename = filename + .as_os_str() + .encode_wide() + .chain([0]) + .collect::>(); let handle = FileHandle(unsafe { CreateFileW( PWSTR(filename.as_ptr() as _), @@ -152,43 +120,12 @@ fn delete_symlink(filename: &[u16]) -> Result<(), runtime::Error> { Ok(()) } -fn is_symlink(filename: &[u16]) -> bool { - let attr = unsafe { GetFileAttributesW(PWSTR(filename.as_ptr() as _)) }; - if attr & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 { - return false; - } - let h_file = FileHandle(unsafe { - CreateFileW( - PWSTR(filename.as_ptr() as _), - FILE_ACCESS_FLAGS(GENERIC_READ), - FILE_SHARE_READ, - std::ptr::null(), - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, - HANDLE::default(), - ) - }); - if h_file.is_invalid() { - return false; - } - let mut buffer = REPARSE_GUID_DATA_BUFFER::default(); - let mut bytes = 0u32; - let result = unsafe { - DeviceIoControl( - h_file.0, - FSCTL_GET_REPARSE_POINT, - std::ptr::null(), - 0, - &mut buffer as *mut _ as _, - std::mem::size_of::() as _, - (&mut bytes) as _, - std::ptr::null_mut(), - ) - }; - if !result.as_bool() && unsafe { GetLastError() } != ERROR_MORE_DATA { - return false; +fn is_symlink(filename: &Path) -> bool { + if let Ok(metadata) = std::fs::symlink_metadata(filename) { + metadata.file_type().is_symlink() + } else { + false } - return buffer.ReparseTag as i32 == IO_REPARSE_TAG_SYMLINK; } struct FileHandle(HANDLE); From a4bd5396d0327d2eb6922d128bff125c7f69494e Mon Sep 17 00:00:00 2001 From: Shirayama Kazatsuyu Date: Sun, 28 Nov 2021 18:13:06 +0900 Subject: [PATCH 31/32] Add Windows specific Env struct. --- src/android/env.rs | 3 +- src/doctor/mod.rs | 3 +- src/doctor/section/android.rs | 2 +- src/doctor/section/device_list.rs | 2 +- src/env.rs | 45 +++---------------- src/os/linux/mod.rs | 2 +- src/os/macos/mod.rs | 2 +- src/os/windows/env.rs | 73 +++++++++++++++++++++++++++++++ src/os/windows/mod.rs | 3 ++ 9 files changed, 89 insertions(+), 46 deletions(-) create mode 100644 src/os/windows/env.rs diff --git a/src/android/env.rs b/src/android/env.rs index 65a931e3..090759c1 100644 --- a/src/android/env.rs +++ b/src/android/env.rs @@ -3,7 +3,8 @@ use super::{ source_props::{self, SourceProps}, }; use crate::{ - env::{Env as CoreEnv, Error as CoreError, ExplicitEnv}, + env::{Error as CoreError, ExplicitEnv}, + os::Env as CoreEnv, util::cli::{Report, Reportable}, }; use std::path::{Path, PathBuf}; diff --git a/src/doctor/mod.rs b/src/doctor/mod.rs index 399f9ff7..153821ab 100644 --- a/src/doctor/mod.rs +++ b/src/doctor/mod.rs @@ -1,7 +1,8 @@ mod section; use crate::{ - env::{self, Env}, + env, + os::Env, util::{self, cli::TextWrapper}, }; use thiserror::Error; diff --git a/src/doctor/section/android.rs b/src/doctor/section/android.rs index 87306fcd..e2d53edd 100644 --- a/src/doctor/section/android.rs +++ b/src/doctor/section/android.rs @@ -1,5 +1,5 @@ use super::Section; -use crate::{android, doctor::Unrecoverable, env::Env, util}; +use crate::{android, doctor::Unrecoverable, os::Env, util}; pub fn check(env: &Env) -> Result { let section = Section::new("Android developer tools"); diff --git a/src/doctor/section/device_list.rs b/src/doctor/section/device_list.rs index a3d8b615..3436736f 100644 --- a/src/doctor/section/device_list.rs +++ b/src/doctor/section/device_list.rs @@ -1,7 +1,7 @@ use super::Section; use crate::{ android::{self, adb}, - env::Env, + os::Env, }; pub fn check(env: &Env) -> Section { diff --git a/src/env.rs b/src/env.rs index 7c26ed56..9e9fdef4 100644 --- a/src/env.rs +++ b/src/env.rs @@ -12,6 +12,8 @@ pub enum Error { HomeNotSet(#[source] std::env::VarError), #[error("The `PATH` environment variable isn't set, which is super weird: {0}")] PathNotSet(#[source] std::env::VarError), + #[error("The `{0}` environment variable isn't set, which is quite weird: {1}")] + NotSet(&'static str, #[source] std::env::VarError), } impl Reportable for Error { @@ -22,42 +24,23 @@ impl Reportable for Error { #[derive(Clone, Debug)] pub struct Env { - home: Option, - userprofile: Option, + home: String, path: String, term: Option, ssh_auth_sock: Option, - system_root: Option, - tmp: Option, - temp: Option, - program_data: Option, } impl Env { pub fn new() -> Result { - let home = std::env::var("HOME"); - let userprofile = std::env::var("USERPROFILE"); - let (home, userprofile) = match (home, userprofile) { - (Err(home_err), Err(_)) => return Err(Error::HomeNotSet(home_err)), - (home, userprofile) => (home.ok(), userprofile.ok()), - }; + let home = std::env::var("HOME").map_err(Error::HomeNotSet)?; let path = std::env::var("PATH").map_err(Error::PathNotSet)?; let term = std::env::var("TERM").ok(); let ssh_auth_sock = std::env::var("SSH_AUTH_SOCK").ok(); - let system_root = std::env::var("SystemRoot").ok(); - let tmp = std::env::var("TMP").ok(); - let temp = std::env::var("TEMP").ok(); - let program_data = std::env::var("ProgramData").ok(); Ok(Self { home, - userprofile, path, term, ssh_auth_sock, - system_root, - tmp, - temp, - program_data, }) } @@ -73,31 +56,13 @@ impl Env { impl ExplicitEnv for Env { fn explicit_env(&self) -> Vec<(&str, &std::ffi::OsStr)> { - let mut env = vec![("PATH", self.path.as_ref())]; - if let Some(home) = self.home.as_ref() { - env.push(("HOME", home.as_ref())); - } - if let Some(userprofile) = self.userprofile.as_ref() { - env.push(("USERPROFILE", userprofile.as_ref())); - } + let mut env = vec![("HOME", self.home.as_ref()), ("PATH", self.path.as_ref())]; if let Some(term) = self.term.as_ref() { env.push(("TERM", term.as_ref())); } if let Some(ssh_auth_sock) = self.ssh_auth_sock.as_ref() { env.push(("SSH_AUTH_SOCK", ssh_auth_sock.as_ref())); } - if let Some(system_root) = self.system_root.as_ref() { - env.push(("SystemRoot", system_root.as_ref())); - } - if let Some(tmp) = self.tmp.as_ref() { - env.push(("TMP", tmp.as_ref())); - } - if let Some(temp) = self.temp.as_ref() { - env.push(("TEMP", temp.as_ref())); - } - if let Some(program_data) = self.program_data.as_ref() { - env.push(("ProgramData", program_data.as_ref())); - } env } } diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs index 6fc79991..5ea5fb02 100644 --- a/src/os/linux/mod.rs +++ b/src/os/linux/mod.rs @@ -8,7 +8,7 @@ use std::{ path::{Path, PathBuf}, }; -pub use crate::util::ln; +pub use crate::{env::Env, util::ln}; #[derive(Debug)] pub enum DetectEditorError { diff --git a/src/os/macos/mod.rs b/src/os/macos/mod.rs index 6bac7f80..2011a122 100644 --- a/src/os/macos/mod.rs +++ b/src/os/macos/mod.rs @@ -15,7 +15,7 @@ use std::{ ptr, }; -pub use crate::util::ln; +pub use crate::{env::Env, util::ln}; // This can hopefully be relied upon... https://stackoverflow.com/q/8003919 static RUST_UTI: &str = "dyn.ah62d4rv4ge81e62"; diff --git a/src/os/windows/env.rs b/src/os/windows/env.rs new file mode 100644 index 00000000..ed4db5ca --- /dev/null +++ b/src/os/windows/env.rs @@ -0,0 +1,73 @@ +use std::{env, path::Path}; + +use crate::env::{Error, ExplicitEnv}; + +#[derive(Debug, Clone)] +pub struct Env { + path: String, + pathext: String, + program_data: String, + system_root: String, + temp: String, + tmp: String, + userprofile: String, + ssh_auth_sock: Option, + term: Option, +} + +impl Env { + pub fn new() -> Result { + let path = env::var("Path").map_err(Error::PathNotSet)?; + let pathext = env::var("PATHEXT").map_err(Error::PathNotSet)?; + let program_data = + env::var("ProgramData").map_err(|err| Error::NotSet("ProgramData", err))?; + let system_root = env::var("SystemRoot").map_err(|err| Error::NotSet("SystemRoot", err))?; + let temp = env::var("TEMP").map_err(|err| Error::NotSet("TEMP", err))?; + let tmp = env::var("TMP").map_err(|err| Error::NotSet("TMP", err))?; + let userprofile = + env::var("USERPROFILE").map_err(|err| Error::NotSet("USERPROFILE", err))?; + let ssh_auth_sock = env::var("SSH_AUTH_SOCK").ok(); + let term = env::var("TERM").ok(); + Ok(Self { + path, + pathext, + program_data, + system_root, + temp, + tmp, + userprofile, + ssh_auth_sock, + term, + }) + } + + pub fn path(&self) -> &str { + &self.path + } + + pub fn prepend_to_path(mut self, path: impl AsRef) -> Self { + self.path = format!("{};{}", path.as_ref().display(), self.path); + self + } +} + +impl ExplicitEnv for Env { + fn explicit_env(&self) -> Vec<(&str, &std::ffi::OsStr)> { + let mut env = vec![ + ("Path", self.path.as_ref()), + ("PATHEXT", self.pathext.as_ref()), + ("ProgramData", self.program_data.as_ref()), + ("SystemRoot", self.system_root.as_ref()), + ("TEMP", self.temp.as_ref()), + ("TMP", self.tmp.as_ref()), + ("USERPROFILE", self.userprofile.as_ref()), + ]; + if let Some(ssh_auth_sock) = self.ssh_auth_sock.as_ref() { + env.push(("SSH_AUTH_SOCK", ssh_auth_sock.as_ref())); + } + if let Some(term) = self.term.as_ref() { + env.push(("TERM", term.as_ref())); + } + env + } +} diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs index e745ed1f..7143c5cb 100644 --- a/src/os/windows/mod.rs +++ b/src/os/windows/mod.rs @@ -1,3 +1,4 @@ +mod env; pub(super) mod info; pub mod ln; @@ -20,6 +21,8 @@ use windows::{ }, }; +pub use env::Env; + #[derive(Debug, Error)] pub enum DetectEditorError { #[error("No default editor is set: AssocQueryStringW for \".rs\" and \".txt\" both failed")] From 398db8c5da6b6cf36df83506fbff7ddefaa8f5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20St=C4=99pie=C5=84?= Date: Mon, 29 Nov 2021 23:41:04 +0100 Subject: [PATCH 32/32] update README.md with information about Windows support --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 8cd2b6ad..fb12e0a7 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,7 @@ The build will probably take a bit, so feel free to go get a snack or something. cargo install --git https://github.com/BrainiumLLC/cargo-mobile ``` -cargo-mobile is currently supported on macOS and Linux. Note that it's not possible to target iOS on platforms other than macOS! You'll still get to target Android either way. - -A PR adding Windows support would be hugely appreciated! +cargo-mobile is currently supported on macOS, Linux and Windows. Note that it's not possible to target iOS on platforms other than macOS! You'll still get to target Android either way. You'll need to have Xcode and the Android SDK/NDK installed. Some of this will ideally be automated in the future, or at least we'll provide a helpful guide and diagnostics.