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 }
diff --git a/Cargo.lock b/Cargo.lock
index fd579fed..be372fd4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -152,8 +152,11 @@ dependencies = [
"bossy",
"cocoa",
"colored",
+ "const-utf16",
"core-foundation",
"deunicode",
+ "dunce",
+ "embed-resource",
"english-numbers",
"env_logger",
"freedesktop_entry_parser",
@@ -179,6 +182,7 @@ dependencies = [
"thiserror",
"toml",
"ureq",
+ "windows",
"yes-or-no",
]
@@ -247,6 +251,12 @@ dependencies = [
"winapi",
]
+[[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"
@@ -300,6 +310,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"
@@ -1268,6 +1295,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"
@@ -1393,6 +1440,58 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "347cdcaae1addebdff584aea1f9fbc0426dedbe1315f1dcf30c7a9876401cd25"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7758986b022add546ae53ccad31f4852ce6bd2e2c2d3cc2b1d7d06dea0b90da"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29261214caab8e589f61031ba1ccd5c3c25e61db2118a3aec4459f58ff798726"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43984fb3b944743142112ae926e7adeccb60f35bb81d43114f4d0fe2871f60ba"
+
+[[package]]
+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 = "dc24ddac19a0cf02ad2b32d8897f202fc1a13ef285e2d4774e6610783cc8398f"
+
+[[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 4ff89bc3..e4287cad 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,31 @@ ureq = "2.2.0"
freedesktop_entry_parser = "1.1"
lexical-core = "0.7.6"
+[target.'cfg(windows)'.dependencies]
+const-utf16 = "0.2.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" }
hit = "0.2.0"
home = "0.5.3"
+
+[target.'cfg(windows)'.build-dependencies]
+embed-resource = "1.6.4"
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.
diff --git a/build.rs b/build.rs
index 94c461fe..a26a8459 100644
--- a/build.rs
+++ b/build.rs
@@ -57,4 +57,14 @@ 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");
+ }
}
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..6cf2b4b5 100644
--- a/src/android/device.rs
+++ b/src/android/device.rs
@@ -8,9 +8,11 @@ use super::{
use crate::{
env::ExplicitEnv as _,
opts::{self, FilterLevel, NoiseLevel, Profile},
+ os::{consts, gradlew_command},
util::{
self,
cli::{Report, Reportable},
+ prefix_path,
},
};
use std::{
@@ -19,11 +21,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)]
@@ -183,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 {
@@ -371,7 +372,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/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/android/jnilibs.rs b/src/android/jnilibs.rs
index fb285f6b..a45b4fad 100644
--- a/src/android/jnilibs.rs
+++ b/src/android/jnilibs.rs
@@ -1,9 +1,10 @@
use super::{config::Config, target::Target};
use crate::{
+ os,
target::TargetTrait as _,
util::{
cli::{Report, Reportable},
- ln,
+ ln, prefix_path,
},
};
use std::path::{Path, PathBuf};
@@ -63,9 +64,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)]
@@ -118,15 +120,13 @@ 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"),
+ );
+ os::ln::force_symlink(src, &dest, ln::TargetStyle::File)
+ .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 2619eeb7..101142f7 100644
--- a/src/android/ndk.rs
+++ b/src/android/ndk.rs
@@ -2,9 +2,12 @@ use super::{
source_props::{self, SourceProps},
target::Target,
};
-use crate::util::{
- cli::{Report, Reportable},
- VersionDouble,
+use crate::{
+ os::consts,
+ util::{
+ cli::{Report, Reportable},
+ VersionDouble,
+ },
};
use once_cell_regex::regex_multi_line;
use std::{
@@ -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,
}
}
}
@@ -257,7 +260,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..c5130895 100644
--- a/src/android/project.rs
+++ b/src/android/project.rs
@@ -6,16 +6,20 @@ use super::{
};
use crate::{
dot_cargo,
+ os::{self, replace_path_separator},
target::TargetTrait as _,
templating::{self, Pack},
util::{
self,
cli::{Report, Reportable, TextWrapper},
- ln,
+ ln, prefix_path,
},
};
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";
@@ -93,7 +97,10 @@ pub fn gen(
|map| {
map.insert(
"root-dir-rel",
- util::relativize_path(config.app().root_dir(), config.project_dir()),
+ 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::>());
@@ -128,6 +135,7 @@ pub fn gen(
.map(|p| p.name.as_str())
.collect::>(),
);
+ map.insert("windows", cfg!(windows));
},
filter.fun(),
)
@@ -170,12 +178,12 @@ pub fn gen(
})?;
}
- let dest = dest.join("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,
})?;
- 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/apple/cli.rs b/src/apple/cli.rs
index a4509e07..e9dc0406 100644
--- a/src/apple/cli.rs
+++ b/src/apple/cli.rs
@@ -145,7 +145,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/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 a09f60cb..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 {
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..5ea5fb02 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::{env::Env, util::ln};
+
#[derive(Debug)]
pub enum DetectEditorError {
NoDefaultEditorSet,
@@ -125,7 +127,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 +166,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 +177,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..2011a122 100644
--- a/src/os/macos/mod.rs
+++ b/src/os/macos/mod.rs
@@ -9,12 +9,14 @@ use core_foundation::{
url::CFURL,
};
use std::{
- ffi::OsStr,
+ ffi::{OsStr, OsString},
fmt::{self, Display},
path::{Path, PathBuf},
ptr,
};
+pub use crate::{env::Env, util::ln};
+
// This can hopefully be relied upon... https://stackoverflow.com/q/8003919
static RUST_UTI: &str = "dyn.ah62d4rv4ge81e62";
@@ -35,6 +37,7 @@ impl Display for DetectEditorError {
pub enum OpenFileError {
PathToUrlFailed { path: PathBuf },
LaunchFailed(OSStatus),
+ BossyLaunchFailed(bossy::Error),
}
impl Display for OpenFileError {
@@ -44,6 +47,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 +100,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 +115,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/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/info.rs b/src/os/windows/info.rs
new file mode 100644
index 00000000..92219321
--- /dev/null
+++ b/src/os/windows/info.rs
@@ -0,0 +1,102 @@
+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;
+
+#[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..ddd1efba
--- /dev/null
+++ b/src/os/windows/ln.rs
@@ -0,0 +1,145 @@
+use crate::util::{
+ ln::{Clobber, Error, ErrorCause, LinkType, TargetStyle},
+ prefix_path,
+};
+use std::{
+ borrow::Cow,
+ fs::{remove_dir_all, remove_file},
+ os::windows::ffi::OsStrExt,
+ path::Path,
+};
+use windows::{
+ runtime::{self, Handle as _},
+ Win32::{
+ Foundation::{CloseHandle, ERROR_PRIVILEGE_NOT_HELD, HANDLE, PWSTR},
+ Storage::FileSystem::{
+ 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,
+ },
+};
+
+pub fn force_symlink(
+ source: impl AsRef,
+ target: impl AsRef,
+ target_style: TargetStyle,
+) -> Result<(), Error> {
+ 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 is_directory = target
+ .parent()
+ .map(|parent| prefix_path(parent, source).is_dir())
+ .unwrap_or(false);
+ 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 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))
+ }
+ })?;
+ 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,
+ rel_source,
+ abs_target.to_owned(),
+ target_style,
+ ErrorCause::MissingFileName,
+ ))
+ }
+ } else {
+ force_symlink(rel_source, abs_target, target_style)
+ }
+}
+
+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 _),
+ 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: &Path) -> bool {
+ if let Ok(metadata) = std::fs::symlink_metadata(filename) {
+ metadata.file_type().is_symlink()
+ } else {
+ false
+ }
+}
+
+struct FileHandle(HANDLE);
+
+impl FileHandle {
+ fn is_invalid(&self) -> bool {
+ self.0.is_invalid()
+ }
+}
+
+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
new file mode 100644
index 00000000..7143c5cb
--- /dev/null
+++ b/src/os/windows/mod.rs
@@ -0,0 +1,287 @@
+mod env;
+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;
+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,
+ },
+ },
+};
+
+pub use env::Env;
+
+#[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}")]
+ IOError(#[source] std::io::Error),
+}
+
+impl From for DetectEditorError {
+ fn from(err: runtime::Error) -> Self {
+ Self::IOError(err.into())
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum OpenFileError {
+ #[error("Launch Failed: {0}")]
+ LaunchFailed(#[source] bossy::Error),
+ #[error("An error occured while calling OS API: {0}")]
+ IOError(#[source] std::io::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::IOError(e.into()));
+ }
+ 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::IOError(runtime::Error::from_win32().into()));
+ }
+ let len = NullTerminatedWTF16Iterator(buffer.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..0e48a57e 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},
};
@@ -54,6 +56,8 @@ impl Display for TargetStyle {
pub enum ErrorCause {
MissingFileName,
CommandFailed(bossy::Error),
+ IOError(std::io::Error),
+ SymlinkNotAllowed,
}
impl Display for ErrorCause {
@@ -63,6 +67,22 @@ 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),
+ 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"
+ )
+ }
}
}
}
@@ -77,6 +97,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!(
@@ -93,6 +133,7 @@ pub struct Call<'a> {
force: Clobber,
source: &'a Path,
target: &'a Path,
+ target_override: Cow<'a, Path>,
target_style: TargetStyle,
}
@@ -104,10 +145,12 @@ impl<'a> Call<'a> {
target: &'a Path,
target_style: TargetStyle,
) -> Result {
- 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 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,
@@ -117,12 +160,15 @@ impl<'a> Call<'a> {
cause: ErrorCause::MissingFileName,
});
}
- }
+ } else {
+ Cow::Borrowed(target)
+ };
Ok(Self {
link_type,
force,
source,
target,
+ target_override,
target_style,
})
}
@@ -140,40 +186,31 @@ impl<'a> Call<'a> {
command.add_arg("-f");
}
Clobber::FileOrDirectory => {
- command.add_arg("-F");
+ if self.target_override.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_override.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_owned(),
target_style: self.target_style,
- cause: ErrorCause::CommandFailed(err),
- })?;
- Ok(())
+ cause,
+ }
}
}
@@ -184,7 +221,7 @@ pub fn force_symlink(
) -> Result<(), Error> {
Call::new(
LinkType::Symbolic,
- Clobber::FileOnly,
+ Clobber::FileOrDirectory,
source.as_ref(),
target.as_ref(),
target_style,
@@ -205,7 +242,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,
diff --git a/src/util/path.rs b/src/util/path.rs
index 69e54bd4..62112697 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;
@@ -39,9 +39,16 @@ pub fn contract_home(path: impl AsRef) -> Result Result {
@@ -77,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(
@@ -185,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));
+ }
+}