diff --git a/README.md b/README.md index 7d661ecf..eecf0164 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ Dinghy is a `cargo` extension to bring cargo workflow to cross-compilation situations. -Dinghy is specifically useful with "small" processor-based devices, like -Android and iOS phones, or small single board computers like the Raspberry Pi. -Situations where native compilation is not possible, or not practical. +Dinghy is specifically useful with "small" processor-based devices, like Android +and iOS phones, or small single board computers like the Raspberry Pi, HarmonyOS NEXT +phones are also supported. Situations where native compilation is not possible, +or not practical. Initially tests and benches were the primary objective of Dinghy, but now at Snips we use it to cross-compile our entire platform. This includes setting @@ -48,7 +49,7 @@ By default, without `-d`, Dinghy will make a native build, just like `cargo` wou Depending on your targets and your workstation, the ease of setting up Dinghy can vary. -* [Android](docs/android.md) is relatively easy, specifically if you already are +* [Android](docs/android.md) and [OpenHarmony](docs/ohos.md) are relatively easy, specifically if you already are a mobile developer. * [iOS](docs/ios.md) setup has a lot of steps, but at least Apple provides everything you will need. Once again, if you are an iOS developer, most of the heavy lifting has diff --git a/dinghy-lib/src/android/device.rs b/dinghy-lib/src/android/device.rs index dce112a3..c29295ef 100644 --- a/dinghy-lib/src/android/device.rs +++ b/dinghy-lib/src/android/device.rs @@ -57,7 +57,7 @@ impl AndroidDevice { return Ok(AndroidDevice { adb, id: id.into(), - supported_targets: supported_targets, + supported_targets, }); } } diff --git a/dinghy-lib/src/lib.rs b/dinghy-lib/src/lib.rs index 81131bdf..7c0ca2de 100644 --- a/dinghy-lib/src/lib.rs +++ b/dinghy-lib/src/lib.rs @@ -9,6 +9,7 @@ mod apple; pub mod config; pub mod device; mod host; +mod ohos; pub mod overlay; pub mod platform; pub mod plugin; @@ -47,6 +48,9 @@ impl Dinghy { if let Some(man) = android::AndroidManager::probe() { managers.push(Box::new(man)); } + if let Some(man) = ohos::OhosManager::probe() { + managers.push(Box::new(man)); + } if let Some(man) = script::ScriptDeviceManager::probe(conf.clone()) { managers.push(Box::new(man)); } diff --git a/dinghy-lib/src/ohos/device.rs b/dinghy-lib/src/ohos/device.rs new file mode 100644 index 00000000..16866b1b --- /dev/null +++ b/dinghy-lib/src/ohos/device.rs @@ -0,0 +1,295 @@ +use crate::device::make_remote_app; +use crate::project::Project; +use crate::utils::{get_current_verbosity, path_to_str, user_facing_log, LogCommandExt}; +use crate::{platform::regular_platform::RegularPlatform, Device, DeviceCompatibility}; +use crate::{Build, BuildBundle}; +use anyhow::{anyhow, bail, Context, Result}; +use log::{debug, info, log_enabled}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{fmt, io}; + +static OHOS_WORK_DIR: &str = "/data/local/tmp/dinghy"; + +#[derive(Clone, Debug)] +pub struct OhosDevice { + pub hdc: PathBuf, + pub id: String, + pub abi_list: Vec, +} + +impl OhosDevice { + fn hdc(&self) -> Command { + let mut command = Command::new(&self.hdc); + command.arg("-t").arg(&self.id); + command + } + + fn to_remote_bundle(build_bundle: &BuildBundle) -> Result { + build_bundle.replace_prefix_with(OHOS_WORK_DIR) + } + + pub fn from_id(hdc: PathBuf, id: String) -> Result { + // https://device.harmonyos.com/en/docs/apiref/doc-guides/faq-debugging-and-running-0000001122066466 + let abi_list = Command::new(&hdc) + .args([ + "-t", + &id, + "shell", + "param", + "get", + "const.product.cpu.abilist", + ]) + .log_invocation(3) + .output()?; + let abi_list = String::from_utf8_lossy(&abi_list.stdout).into_owned(); + // Filter out hdc log(`[W][2024-02-15 16:34:34] FreeChannelContinue handle->data is nullptr`) and new line + let abi_list = abi_list + .lines() + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .filter(|x| !x.starts_with('[')) + .next() + .context("Get ohos abi list failed.")?; + debug!( + "OpenHarmony device {}, get abi list returned `{}`", + id, abi_list, + ); + let abi_list = if abi_list == "default" { + let output = Command::new(&hdc) + .args(["-t", &id, "shell", "ls", "/system/"]) + .output()? + .stdout; + let output = String::from_utf8_lossy(&output); + let lib64 = output + .split(['\n', ' ']) + .filter(|x| !x.is_empty()) + .any(|x| x == "lib64"); + if lib64 { + vec!["arm64-v8a".to_string()] + } else { + vec!["armeabi".to_string(), "armeabi-v7a".to_string()] + } + } else { + abi_list + .split(",") + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .map(|x| x.to_string()) + .collect() + }; + Ok(OhosDevice { hdc, id, abi_list }) + } + + fn install_app(&self, project: &Project, build: &Build) -> Result<(BuildBundle, BuildBundle)> { + info!("Install {} to {}", build.runnable.id, self.id); + user_facing_log( + "Installing", + &format!("{} to {}", build.runnable.id, self.id), + 0, + ); + if !self + .hdc() + .arg("shell") + .arg("mkdir") + .arg("-p") + .arg(OHOS_WORK_DIR) + .log_invocation(2) + .status()? + .success() + { + bail!( + "Failure to create dinghy work dir '{:?}' on target ohos device", + OHOS_WORK_DIR + ) + } + + let build_bundle = make_remote_app(project, build)?; + let remote_bundle = OhosDevice::to_remote_bundle(&build_bundle)?; + + self.sync( + &build_bundle.bundle_dir, + &remote_bundle + .bundle_dir + .parent() + .ok_or_else(|| anyhow!("Invalid path {}", remote_bundle.bundle_dir.display()))?, + )?; + self.sync( + &build_bundle.lib_dir, + &remote_bundle + .lib_dir + .parent() + .ok_or_else(|| anyhow!("Invalid path {}", remote_bundle.lib_dir.display()))?, + )?; + + debug!("Chmod target exe {}", remote_bundle.bundle_exe.display()); + if !self + .hdc() + .arg("shell") + .arg("chmod") + .arg("755") + .arg(&remote_bundle.bundle_exe) + .log_invocation(2) + .status()? + .success() + { + bail!("Failure in ohos install"); + } + Ok((build_bundle, remote_bundle)) + } + + fn sync, TP: AsRef>(&self, from_path: FP, to_path: TP) -> Result<()> { + let mut command = self.hdc(); + command + .arg("file") + .arg("send") + .arg("-sync") + .arg(from_path.as_ref()) + .arg(to_path.as_ref()); + if !log_enabled!(::log::Level::Debug) { + command.stdout(::std::process::Stdio::null()); + command.stderr(::std::process::Stdio::null()); + } + debug!("Running {:?}", command); + if !command.log_invocation(2).status()?.success() { + bail!("Error syncing ohos directory ({:?})", command) + } else { + Ok(()) + } + } +} + +impl fmt::Display for OhosDevice { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "OpenHarmony/{}", self.id) + } +} + +impl DeviceCompatibility for OhosDevice { + fn is_compatible_with_regular_platform(&self, platform: &RegularPlatform) -> bool { + if let Some(abi) = platform.id.strip_prefix("auto-ohos-") { + self.abi_list.iter().any(|x| x == abi) + } else { + false + } + } +} + +impl Device for OhosDevice { + fn clean_app(&self, build_bundle: &BuildBundle) -> Result<()> { + let remote_bundle = OhosDevice::to_remote_bundle(build_bundle)?; + debug!("Cleaup device"); + if !self + .hdc() + .arg("shell") + .arg("rm") + .arg("-rf") + .arg(&remote_bundle.bundle_dir) + .log_invocation(1) + .status()? + .success() + { + bail!("Failure in ohos clean") + } + if !self + .hdc() + .arg("shell") + .arg("rm") + .arg("-rf") + .arg(&remote_bundle.lib_dir) + .log_invocation(1) + .status()? + .success() + { + bail!("Failure in ohos clean") + } + Ok(()) + } + + fn debug_app( + &self, + _project: &Project, + _build: &Build, + _args: &[&str], + _envs: &[&str], + ) -> Result { + unimplemented!() + } + + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> &str { + "OpenHarmony device" + } + + fn run_app( + &self, + project: &Project, + build: &Build, + args: &[&str], + envs: &[&str], + ) -> Result { + let args: Vec = args + .iter() + .map(|&a| ::shell_escape::escape(a.into()).to_string()) + .collect(); + let (build_bundle, remote_bundle) = self.install_app(&project, &build)?; + let command = format!( + "cd '{}'; RUST_BACKTRACE=1 {} DINGHY=1 LD_LIBRARY_PATH=\"{}:$LD_LIBRARY_PATH\" {} {} ; echo FORWARD_RESULT_TO_DINGHY_BECAUSE_HDC_DOES_NOT=$?", + path_to_str(&remote_bundle.bundle_dir)?, + envs.join(" "), + path_to_str(&remote_bundle.lib_dir)?, + path_to_str(&remote_bundle.bundle_exe)?, + args.join(" ")); + info!("Run {} on {}", build.runnable.id, self.id); + + if get_current_verbosity() < 1 { + // we log the full command for verbosity > 1, just log a short message when the user + // didn't ask for verbose output + user_facing_log( + "Running", + &format!("{} on {}", build.runnable.id, self.id), + 0, + ); + } + + if !self + .hdc() + .arg("shell") + .arg(&command) + .log_invocation(1) + .output() + .with_context(|| format!("Couldn't run {} using hdc.", build.runnable.exe.display())) + .and_then(|output| { + if output.status.success() { + let _ = io::stdout().write(output.stdout.as_slice()); + let _ = io::stderr().write(output.stderr.as_slice()); + String::from_utf8(output.stdout).with_context(|| { + format!("Couldn't run {} using hdc.", build.runnable.exe.display()) + }) + } else { + bail!("Couldn't run {} using hdc.", build.runnable.exe.display()) + } + }) + .map(|output| { + output + .lines() + // Filter out hdc logs + .filter(|x| !x.starts_with('[')) + .last() + .unwrap_or("") + .to_string() + }) + .map(|last_line| { + last_line.contains("FORWARD_RESULT_TO_DINGHY_BECAUSE_HDC_DOES_NOT=0") + })? + { + bail!("Failed") + } + + Ok(build_bundle) + } +} diff --git a/dinghy-lib/src/ohos/mod.rs b/dinghy-lib/src/ohos/mod.rs new file mode 100644 index 00000000..2f5005de --- /dev/null +++ b/dinghy-lib/src/ohos/mod.rs @@ -0,0 +1,169 @@ +mod device; +mod platform; + +use crate::{ + config::PlatformConfiguration, + ohos::platform::{OhosArch, OhosPlatform}, + toolchain::ToolchainConfig, + utils::LogCommandExt, + Device, Platform, PlatformManager, +}; +use anyhow::{anyhow, bail, Context, Result}; +use device::OhosDevice; +use log::debug; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; + +pub struct OhosManager { + hdc: PathBuf, +} + +impl PlatformManager for OhosManager { + fn devices(&self) -> Result>> { + let result = Command::new(&self.hdc) + .arg("list") + .arg("targets") + .log_invocation(3) + .output() + .context("Run hdc failed.")?; + let output = String::from_utf8_lossy(&result.stdout).trim().to_string(); + if output == "[Empty]" { + return Ok(Vec::new()); + } + let mut devices = Vec::new(); + // Filter out hdc log and new line + for id in output + .lines() + .map(|x| x.trim()) + .filter(|x| !x.starts_with("[")) + .filter(|x| !x.is_empty()) + { + let device = OhosDevice::from_id(self.hdc.clone(), id.to_string()) + .context("Create OpenHarmony device from id failed.")?; + debug!("Discovered OpenHarmony device: ({:?})", device); + devices.push(Box::new(device) as Box); + } + Ok(devices) + } + + fn platforms(&self) -> Result>> { + let ndk = ohos_ndk().context("Find ohos ndk path failed.")?; + let tools = ndk.join("llvm"); + let sysroot = ndk.join("sysroot"); + let bin_dir = tools.join("bin"); + let version = ohos_ndk_version(&ndk).context("Get ndk version failed.")?; + let ndk_major_version: usize = version + .split(".") + .next() + .and_then(|major| major.parse().ok()) + .ok_or_else(|| anyhow!("Invalid version found for ohos ndk {:?}", &ndk))?; + debug!( + "OpenHarmony ndk: {:?}, ndk version: {}, ndk_major_version: {}", + ndk, version, ndk_major_version + ); + let mut platforms = vec![]; + for (arch, rustc_cpu, cc_cpu, binutils_cpu, abi, abi_kind) in [ + ( + OhosArch::Aarch64, + "aarch64", + "aarch64", + "aarch64", + "arm64-v8a", + "ohos", + ), + ( + OhosArch::Armv7, + "armv7", + "armv7a", + "arm", + "armeabi-v7a", + "ohos", + ), + ( + OhosArch::X86_64, + "x86_64", + "x86_64", + "x86_64", + "x86_64", + "ohos", + ), + ] { + let id = format!("auto-ohos-{}", abi); + let toolchain_config = ToolchainConfig { + bin_dir: bin_dir.clone(), + rustc_triple: format!("{}-unknown-linux-{}", rustc_cpu, abi_kind), + // pkgconfig is placed in `native/llvm/python3/lib/pkgconfig/` + root: tools.clone(), + sysroot: Some(sysroot.clone()), + cc: "clang".to_string(), + cxx: "clang++".to_string(), + binutils_prefix: format!("{}-linux-{}", binutils_cpu, abi_kind), + cc_prefix: format!("{}-linux-{}", cc_cpu, abi_kind), + }; + platforms.push( + OhosPlatform::new( + PlatformConfiguration::default(), + arch, + id, + toolchain_config, + ndk_major_version, + ndk.clone(), + ) + .context("Create ohos platform failed.")?, + ); + } + Ok(platforms) + } +} + +impl OhosManager { + pub fn probe() -> Option { + match hdc() { + Ok(hdc) => { + debug!("HDC found: {:?}", hdc); + Some(OhosManager { hdc }) + } + Err(_) => { + debug!("hdc not found in path, ohos disabled"); + None + } + } + } +} + +fn hdc() -> Result { + if let Ok(hdc) = std::env::var("DINGHY_OHOS_HDC") { + return Ok(hdc.into()); + } + if let Ok(hdc) = ::which::which("hdc") { + return Ok(hdc); + } + if let Ok(ndk) = std::env::var("OHOS_SDK_HOME") { + return Ok(Path::new(&ndk).join("default/openharmony/toolchains/hdc")); + } + bail!("The hdc couldn't be found") +} + +fn ohos_ndk() -> Result { + if let Ok(sdk) = std::env::var("OHOS_SDK_HOME") { + return Ok(Path::new(&sdk).join("default/openharmony/native/")); + } + bail!("The ndk couldn't be found") +} + +fn ohos_ndk_version(ndk: &Path) -> Result { + let meta_path = ndk.join("oh-uni-package.json"); + let meta = fs::read_to_string(&meta_path) + .with_context(|| anyhow!("Read NDK meta file failed: {}", meta_path.display()))?; + let mut meta = json::parse(&meta) + .with_context(|| anyhow!("Parse ohos ndk meta file failed: {}", meta_path.display()))?; + let ndk_version = meta + .remove("version") + .as_str() + .context("No version in oh-uni-package.json file")? + .to_string(); + Ok(ndk_version) +} diff --git a/dinghy-lib/src/ohos/platform.rs b/dinghy-lib/src/ohos/platform.rs new file mode 100644 index 00000000..abd30f9f --- /dev/null +++ b/dinghy-lib/src/ohos/platform.rs @@ -0,0 +1,151 @@ +use crate::platform::regular_platform::RegularPlatform; +use crate::toolchain::{create_shim, ToolchainConfig}; +use crate::Result; +use crate::{Build, Device, Platform, PlatformConfiguration, Project, SetupArgs}; +use anyhow::{anyhow, Context}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Copy)] +pub enum OhosArch { + Aarch64, + Armv7, + X86_64, +} + +#[derive(Debug)] +pub struct OhosPlatform { + regular_platform: Box, + arch: OhosArch, + toolchain_config: ToolchainConfig, + /// Will use it some day, it's inevitable. + #[allow(dead_code)] + ndk_major_version: usize, + ndk_path: PathBuf, +} + +impl OhosPlatform { + pub fn new( + configuration: PlatformConfiguration, + arch: OhosArch, + id: String, + toolchain_config: ToolchainConfig, + ndk_major_version: usize, + ndk_path: PathBuf, + ) -> Result> { + Ok(Box::new(Self { + regular_platform: RegularPlatform::new_with_tc( + configuration, + id, + toolchain_config.clone(), + )?, + arch, + toolchain_config, + ndk_major_version, + ndk_path, + })) + } +} + +// see https://doc.rust-lang.org/rustc/platform-support/openharmony.html +fn ohos_ndk_tool_wrapper(arch: OhosArch, tool: &str, ndk_path: &Path) -> String { + let tools_path = format!("{}/llvm/bin/{}", ndk_path.display(), tool); + let sysroot = format!("{}/sysroot", ndk_path.display()); + let content = match arch { + OhosArch::Aarch64 => format!( + r###" +exec "{tools_path}" \ + -target aarch64-linux-ohos \ + --sysroot="{sysroot}" \ + -D__MUSL__ \ + "$@" +"###, + ), + + OhosArch::Armv7 => format!( + r###" +exec "{tools_path}" \ + -target arm-linux-ohos \ + --sysroot="{sysroot}" \ + -D__MUSL__ \ + -march=armv7-a \ + -mfloat-abi=softfp \ + -mtune=generic-armv7-a \ + -mthumb \ + "$@" +"###, + ), + OhosArch::X86_64 => format!( + r###" +exec "{tools_path}" \ + -target x86_64-linux-ohos \ + --sysroot="{sysroot}" \ + -D__MUSL__ \ + "$@" +"###, + ), + }; + content +} + +impl Platform for OhosPlatform { + fn setup_env(&self, project: &Project, setup_args: &SetupArgs) -> anyhow::Result<()> { + self.regular_platform.setup_env(project, setup_args)?; + + let shim_creator = |tool: &str| { + let content = ohos_ndk_tool_wrapper(self.arch, tool, &self.ndk_path); + create_shim( + &project.metadata.workspace_root, + self.regular_platform.rustc_triple(), + &self.regular_platform.id(), + tool, + &content, + ) + .with_context(|| anyhow!("Create {} shim failed.", tool)) + }; + + let cc = shim_creator("clang").context("Create clang shim failed.")?; + let cxx = shim_creator("clang++").context("Create clang++ shim failed.")?; + self.toolchain_config + .setup_tool("CC", &cc.to_string_lossy())?; + self.toolchain_config + .setup_linker_raw(&cc.to_string_lossy()); + self.toolchain_config + .setup_tool("CXX", &cxx.to_string_lossy())?; + self.toolchain_config + .setup_tool("CPP", &cxx.to_string_lossy())?; + self.toolchain_config + .setup_tool("AR", &self.toolchain_config.naked_executable("llvm-ar"))?; + self.toolchain_config + .setup_tool("AS", &self.toolchain_config.naked_executable("llvm-as"))?; + self.toolchain_config.setup_tool( + "RANLIB", + &self.toolchain_config.naked_executable("llvm-ranlib"), + )?; + + Ok(()) + } + + fn id(&self) -> String { + self.regular_platform.id() + } + + fn is_compatible_with(&self, device: &dyn Device) -> bool { + self.regular_platform.is_compatible_with(device) + } + + fn is_host(&self) -> bool { + self.regular_platform.is_host() + } + + fn rustc_triple(&self) -> &str { + self.regular_platform.rustc_triple() + } + + fn strip(&self, build: &mut Build) -> anyhow::Result<()> { + self.regular_platform.strip(build) + } + + fn sysroot(&self) -> anyhow::Result> { + self.regular_platform.sysroot() + } +} diff --git a/dinghy-lib/src/toolchain.rs b/dinghy-lib/src/toolchain.rs index 3d92f74b..cba072ed 100644 --- a/dinghy-lib/src/toolchain.rs +++ b/dinghy-lib/src/toolchain.rs @@ -36,6 +36,13 @@ impl Toolchain { Ok(()) } + pub fn setup_linker_raw(&self, linker: &str) { + set_env( + format!("CARGO_TARGET_{}_LINKER", envify(self.rustc_triple.as_str())), + linker, + ); + } + pub fn setup_linker>( &self, id: &str, @@ -161,6 +168,10 @@ impl ToolchainConfig { linker_cmd } + pub fn setup_linker_raw(&self, linker: &str) { + self.as_toolchain().setup_linker_raw(linker) + } + pub fn setup_linker>( &self, id: &str, @@ -220,7 +231,7 @@ impl ToolchainConfig { } } -fn create_shim>( +pub fn create_shim>( root: P, rustc_triple: &str, id: &str, diff --git a/docs/ohos.md b/docs/ohos.md new file mode 100644 index 00000000..80b26f5c --- /dev/null +++ b/docs/ohos.md @@ -0,0 +1,83 @@ +## Getting started - OpenHarmony(HarmonyOS Next) phone + +### Dinghy setup + +Assuming [rustup](http://rustup.rs) is already installed... + +``` +cargo install cargo-dinghy + +# If it's already installed, add '--force' +cargo install cargo-dinghy --force +``` + +### HarmonyOS SDK + +First, download the [HarmonyOS SDK](https://developer.huawei.com/consumer/cn/download/). Make sure to set the environment variable `OHOS_SDK_HOME` to point to the extracted SDK folder, you can check if it's set correctly by running: `file $OHOS_SDK_HOME/default/openharmony/toolchains/hdc`. + +If you did everything correctly, Dinghy should be able to recognize a large quantities of platforms. The following is an example of what you should see : + +``` +% cargo dinghy all-platforms +[...] +* auto-ohos-arm64-v8a aarch64-unknown-linux-ohos +* auto-ohos-armeabi-v7a armv7-unknown-linux-ohos +* auto-ohos-x86_64 x86_64-unknown-linux-ohos +[...] +``` + +If you get all the platforms, your SDK is set up. To finish your setup, you should [install the appropriate Rust target](#rust-target). + +### See your OpenHarmony devices + +`$OHOS_SDK_HOME` should be set to the path of HarmonyOS SDK, and your phone must have debugging enabled. If you don't have a phone, you can run an emulator(Download and run [DevEco Studio](https://developer.huawei.com/consumer/cn/download/), then create a new emulator by navigating to `Tools -> Device Manager -> New Emulator`). +See [hdc doc](https://gitee.com/openharmony/docs/blob/master/en/device-dev/subsystems/subsys-toolchain-hdc-guide.md). +`hdc list targets` must show your phone/emulator like this: + +``` +% $OHOS_SDK_HOME/default/openharmony/toolchains/hdc list targets +127.0.0.1:5555 +``` + +Now dinghy should also "see" your phone: + +``` +% cargo dinghy all-devices +List of available devices for all platforms: +OpenHarmony/127.0.0.1:5555: [OhosPlatform { regular_platform: auto-ohos-arm64-v8a, arch: Aarch64, .. } ] +``` + +### Rust target + +After you set up the SDK, you may need to ask rustup to install the relevant target: + +``` +rustup target install aarch64-unknown-linux-ohos +``` + +AArch64 is the most likely architecture for OpenHarmony devices. However, maybe yours is one of these (two last are very unlikely): + +``` +rustup target install armv7-unknown-linux-ohos +rustup target install x86_64-unknown-linux-ohos +``` + +### Try it + +Let's try it with the Dinghy demo project. The project tests with "pass" in the name is supposed to pass, the one with "fail" should break. + +(Worth noting that `-Zbuild-std` is required for old Rust toolchains) + +``` +% git clone https://github.com/sonos/dinghy +% cd dinghy/test-ws +[...] +# these ones should pass +% cargo dinghy -d ohos test pass -Zbuild-std +[...] +# this one shall not pass +% cargo dinghy -d ohos test fail -Zbuild-std +[...] +``` + +That's it! Enjoy! diff --git a/test-ws/test-proc-macro/src/lib.rs b/test-ws/test-proc-macro/src/lib.rs index e69de29b..8b137891 100644 --- a/test-ws/test-proc-macro/src/lib.rs +++ b/test-ws/test-proc-macro/src/lib.rs @@ -0,0 +1 @@ +