diff --git a/Cargo.lock b/Cargo.lock index 0a7548db..aa77319d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,7 @@ dependencies = [ "colored", "core-foundation 0.10.0", "deunicode", + "dirs", "duct", "dunce", "embed-resource", @@ -454,6 +455,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + [[package]] name = "duct" version = "1.0.0" @@ -882,6 +904,16 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags 2.4.1", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1038,6 +1070,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_info" version = "3.8.2" @@ -1253,6 +1291,17 @@ dependencies = [ "getrandom 0.2.12", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.12", + "libredox", + "thiserror 2.0.11", +] + [[package]] name = "regex" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index b00e3eac..5d8020db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ toml = { version = "0.9", features = ["preserve_order"] } duct = "1" which = "8" os_pipe = "1" +dirs = "6" [dev-dependencies] rstest = "0.26" diff --git a/src/config/metadata.rs b/src/config/metadata.rs index 838ed76e..1253cdbf 100644 --- a/src/config/metadata.rs +++ b/src/config/metadata.rs @@ -30,6 +30,8 @@ pub struct Metadata { pub apple: crate::apple::config::Metadata, #[serde(default, rename = "cargo-android")] pub android: crate::android::config::Metadata, + #[serde(default, rename = "cargo-open-harmony")] + pub open_harmony: crate::open_harmony::config::Metadata, } impl Metadata { @@ -63,4 +65,8 @@ impl Metadata { pub fn android(&self) -> &crate::android::config::Metadata { &self.android } + + pub fn open_harmony(&self) -> &crate::open_harmony::config::Metadata { + &self.open_harmony + } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 29e1aab6..1874d915 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -7,7 +7,7 @@ use self::{app::App, raw::*}; #[cfg(target_os = "macos")] use crate::apple; use crate::{ - android, bicycle, templating, + android, bicycle, open_harmony, templating, util::cli::{Report, Reportable, TextWrapper}, }; use serde::Serialize; @@ -31,6 +31,8 @@ pub enum FromRawError { AppleConfigInvalid(apple::config::Error), #[error(transparent)] AndroidConfigInvalid(android::config::Error), + #[error(transparent)] + OpenHarmonyConfigInvalid(open_harmony::config::Error), } impl FromRawError { @@ -40,6 +42,7 @@ impl FromRawError { #[cfg(target_os = "macos")] Self::AppleConfigInvalid(err) => err.report(msg), Self::AndroidConfigInvalid(err) => err.report(msg), + Self::OpenHarmonyConfigInvalid(err) => err.report(msg), } } } @@ -99,6 +102,7 @@ pub struct Config { #[cfg(target_os = "macos")] apple: apple::config::Config, android: android::config::Config, + open_harmony: open_harmony::config::Config, } impl Config { @@ -109,11 +113,14 @@ impl Config { .map_err(FromRawError::AppleConfigInvalid)?; let android = android::config::Config::from_raw(app.clone(), raw.android) .map_err(FromRawError::AndroidConfigInvalid)?; + let open_harmony = open_harmony::config::Config::from_raw(app.clone(), raw.open_harmony) + .map_err(FromRawError::OpenHarmonyConfigInvalid)?; Ok(Self { app, #[cfg(target_os = "macos")] apple, android, + open_harmony, }) } @@ -175,6 +182,10 @@ impl Config { &self.android } + pub fn open_harmony(&self) -> &open_harmony::config::Config { + &self.open_harmony + } + pub fn build_a_bike(&self) -> bicycle::Bicycle { templating::init(Some(self)) } diff --git a/src/config/raw.rs b/src/config/raw.rs index 00186866..1171e293 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -2,7 +2,7 @@ use super::app; #[cfg(target_os = "macos")] use crate::apple; use crate::{ - android, + android, open_harmony, util::cli::{Report, Reportable, TextWrapper}, }; use serde::{Deserialize, Serialize}; @@ -77,6 +77,7 @@ pub struct Raw { #[cfg(target_os = "macos")] pub apple: Option, pub android: Option, + pub open_harmony: Option, } impl Raw { @@ -89,6 +90,7 @@ impl Raw { #[cfg(target_os = "macos")] apple: Some(apple), android: None, + open_harmony: None, }) } @@ -101,6 +103,7 @@ impl Raw { #[cfg(target_os = "macos")] apple: Some(apple), android: None, + open_harmony: None, }) } diff --git a/src/init.rs b/src/init.rs index 65d7780b..1dece5e8 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,6 +1,6 @@ -use crate::android; #[cfg(target_os = "macos")] use crate::apple; +use crate::{android, open_harmony}; use crate::{ config::{ self, @@ -65,6 +65,8 @@ pub enum Error { cause: io::Error, }, OpenInEditorFailed(util::OpenInEditorError), + OpenHarmonyEnvFailed(open_harmony::env::Error), + OpenHarmonyInitFailed(open_harmony::project::Error), } impl Reportable for Error { @@ -87,6 +89,8 @@ impl Reportable for Error { Self::DotCargoWriteFailed(err) => err.report(), Self::DotFirstInitDeleteFailed { path, cause } => Report::action_request(format!("Failed to delete first init dot file {path:?}; the project generated successfully, but `cargo mobile init` will have unexpected results unless you manually delete this file!"), cause), Self::OpenInEditorFailed(err) => Report::error("Failed to open project in editor (your project generated successfully though, so no worries!)", err), + Self::OpenHarmonyEnvFailed(err) => err.report(), + Self::OpenHarmonyInitFailed(err) => err.report(), } } } @@ -204,6 +208,37 @@ pub fn exec( ); } + // Generate DevEco Studio project + if metadata.open_harmony().supported() { + match open_harmony::env::Env::new() { + Ok(env) => open_harmony::project::gen( + config.open_harmony(), + metadata.open_harmony(), + &env, + &bike, + wrapper, + &filter, + skip_targets_install, + ) + .map_err(Error::OpenHarmonyInitFailed)?, + Err(err) => { + if err.sdk_issue() { + Report::action_request( + "Failed to initialize OpenHarmony environment; OpenHarmony support won't be usable until you fix the issue below and re-run `cargo mobile init`!", + err, + ) + .print(wrapper); + } else { + Err(Error::OpenHarmonyEnvFailed(err))?; + } + } + } + } else { + println!( + "Skipping OpenHarmony init, since it's marked as unsupported in your Cargo.toml metadata" + ); + } + dot_cargo .write(config.app()) .map_err(Error::DotCargoWriteFailed)?; diff --git a/src/lib.rs b/src/lib.rs index 7e1a5851..a14e5a81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod doctor; pub mod dot_cargo; pub mod env; pub mod init; +pub mod open_harmony; pub mod opts; pub mod os; mod project; diff --git a/src/open_harmony/cli.rs b/src/open_harmony/cli.rs new file mode 100644 index 00000000..cf1c6f7f --- /dev/null +++ b/src/open_harmony/cli.rs @@ -0,0 +1,282 @@ +use crate::{ + config::{ + metadata::{self, Metadata as OmniMetadata}, + Config as OmniConfig, LoadOrGenError, + }, + define_device_prompt, + device::PromptError, + open_harmony::{ + config::{Config, Metadata}, + device::{Device, RunError, StacktraceError}, + env::{Env, Error as EnvError}, + hap, hdc, + target::{BuildError, CompileLibError, Target}, + NAME, + }, + os, + target::{call_for_targets_with_fallback, TargetInvalid, TargetTrait as _}, + util::{ + cli::{ + self, Exec, GlobalFlags, Report, Reportable, TextWrapper, VERSION_LONG, VERSION_SHORT, + }, + prompt, + }, +}; +use std::{ffi::OsString, path::PathBuf}; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt( + bin_name = cli::bin_name(NAME), + version = VERSION_SHORT, + long_version = VERSION_LONG.as_str(), + global_settings = cli::GLOBAL_SETTINGS, + settings = cli::SETTINGS, +)] +pub struct Input { + #[structopt(flatten)] + flags: GlobalFlags, + #[structopt(subcommand)] + command: Command, +} + +impl Input { + pub fn new(flags: GlobalFlags, command: Command) -> Self { + Self { flags, command } + } +} + +#[derive(Clone, Debug, StructOpt)] +pub enum Command { + #[structopt(name = "open", about = "Open project in DevEco Studio")] + Open, + #[structopt(name = "check", about = "Checks if code compiles for target(s)")] + Check { + #[structopt(name = "targets", default_value = Target::DEFAULT_KEY, possible_values = &Target::name_list())] + targets: Vec, + }, + #[structopt(name = "build", about = "Builds dynamic libraries for target(s)")] + Build { + #[structopt(name = "targets", default_value = Target::DEFAULT_KEY, possible_values = &Target::name_list())] + targets: Vec, + #[structopt(flatten)] + profile: cli::Profile, + }, + #[structopt(name = "run", about = "Deploys HAP to connected device")] + Run { + #[structopt(flatten)] + profile: cli::Profile, + }, + #[structopt(name = "st", about = "Displays a detailed stacktrace for a device")] + Stacktrace, + #[structopt(name = "list", about = "Lists connected devices")] + List, + #[structopt(name = "hap", about = "Manage and build HAPs")] + Hap { + #[structopt(subcommand)] + cmd: HapSubcommand, + }, +} + +#[derive(StructOpt, Clone, Debug)] +pub enum HapSubcommand { + #[structopt(about = "build HAP (Harmony Ability Package)")] + Build { + #[structopt(flatten)] + profile: cli::Profile, + }, +} + +#[derive(Debug)] +pub enum Error { + EnvInitFailed(EnvError), + DevicePromptFailed(PromptError), + TargetInvalid(TargetInvalid), + ConfigFailed(LoadOrGenError), + MetadataFailed(metadata::Error), + Unsupported, + ProjectDirAbsent { project_dir: PathBuf }, + OpenFailed(os::OpenFileError), + CheckFailed(CompileLibError), + BuildFailed(BuildError), + RunFailed(RunError), + StacktraceFailed(StacktraceError), + ListFailed(hdc::device_list::Error), + HapError(hap::HapError), +} + +impl Reportable for Error { + fn report(&self) -> Report { + match self { + Self::EnvInitFailed(err) => err.report(), + Self::DevicePromptFailed(err) => err.report(), + Self::TargetInvalid(err) => Report::error("Specified target was invalid", err), + Self::ConfigFailed(err) => err.report(), + Self::MetadataFailed(err) => err.report(), + Self::Unsupported => Report::error("OpenHarmony is marked as unsupported in your Cargo.toml metadata", "If your project should support OpenHarmony, modify your Cargo.toml, then run `cargo mobile init` and try again."), + Self::ProjectDirAbsent { project_dir } => Report::action_request( + "Please run `cargo mobile init` and try again!", + format!( + "DevEco Studio project directory {:?} doesn't exist.", + project_dir + ), + ), + Self::OpenFailed(err) => Report::error("Failed to open project in DevEco Studio", err), + Self::CheckFailed(err) => err.report(), + Self::BuildFailed(err) => err.report(), + Self::RunFailed(err) => err.report(), + Self::StacktraceFailed(err) => err.report(), + Self::ListFailed(err) => err.report(), + Self::HapError(err) => err.report(), + } + } +} + +impl Exec for Input { + type Report = Error; + + fn global_flags(&self) -> GlobalFlags { + self.flags + } + + fn exec(self, wrapper: &TextWrapper) -> Result<(), Self::Report> { + define_device_prompt!(hdc::device_list, hdc::device_list::Error, OpenHarmony); + fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { + device_prompt(env).map(|device| device.target()).ok() + } + + fn with_config( + non_interactive: bool, + wrapper: &TextWrapper, + f: impl FnOnce(&Config, &Metadata, &Env) -> Result<(), Error>, + ) -> Result<(), Error> { + let (config, _origin) = OmniConfig::load_or_gen(".", non_interactive, wrapper) + .map_err(Error::ConfigFailed)?; + let metadata = + OmniMetadata::load(config.app().root_dir()).map_err(Error::MetadataFailed)?; + let mut env = Env::new().map_err(Error::EnvInitFailed)?; + + if let Some(vars) = metadata.open_harmony().env_vars.as_ref() { + env.base = env.base.explicit_env_vars( + vars.iter() + .map(|d| { + ( + d.0.to_owned(), + OsString::from( + d.1.replace( + "", + &dunce::simplified(&config.open_harmony().project_dir()) + .to_string_lossy(), + ), + ), + ) + }) + .collect::>(), + ); + } + + if metadata.open_harmony().supported() { + f(config.open_harmony(), metadata.open_harmony(), &env) + } else { + Err(Error::Unsupported) + } + } + + fn ensure_init(config: &Config) -> Result<(), Error> { + if !config.project_dir_exists() { + Err(Error::ProjectDirAbsent { + project_dir: config.project_dir(), + }) + } else { + Ok(()) + } + } + + fn open_in_dev_eco_studio(config: &Config, env: &Env) -> Result<(), Error> { + os::open_file_with("DevEco-Studio", config.project_dir(), &env.base) + .map_err(Error::OpenFailed) + } + + let Self { + flags: + GlobalFlags { + noise_level, + non_interactive, + }, + command, + } = self; + match command { + Command::Open => with_config(non_interactive, wrapper, |config, _, env| { + ensure_init(config)?; + open_in_dev_eco_studio(config, env) + }), + Command::Check { targets } => { + with_config(non_interactive, wrapper, |config, metadata, env| { + let force_color = true; + call_for_targets_with_fallback( + targets.iter(), + &detect_target_ok, + env, + |target: &Target| { + target + .check(config, metadata, env, noise_level, force_color) + .map_err(Error::CheckFailed) + }, + ) + .map_err(Error::TargetInvalid)? + }) + } + Command::Build { + targets, + profile: cli::Profile { profile }, + } => with_config(non_interactive, wrapper, |config, metadata, env| { + ensure_init(config)?; + let force_color = true; + call_for_targets_with_fallback( + targets.iter(), + &detect_target_ok, + env, + |target: &Target| { + target + .build(config, metadata, env, noise_level, force_color, profile) + .map_err(Error::BuildFailed) + }, + ) + .map_err(Error::TargetInvalid)? + }), + Command::Run { + profile: cli::Profile { profile }, + } => with_config(non_interactive, wrapper, |config, _metadata, env| { + ensure_init(config)?; + device_prompt(env) + .map_err(Error::DevicePromptFailed)? + .run(config, env, noise_level, profile) + .and_then(|h| h.wait().map(|_| ()).map_err(Into::into)) + .map_err(Error::RunFailed) + }), + Command::Stacktrace => with_config(non_interactive, wrapper, |config, _, env| { + ensure_init(config)?; + device_prompt(env) + .map_err(Error::DevicePromptFailed)? + .stacktrace(config, env) + .map_err(Error::StacktraceFailed) + }), + Command::List => with_config(non_interactive, wrapper, |_, _, env| { + hdc::device_list(env) + .map_err(Error::ListFailed) + .map(|device_list| { + prompt::list_display_only(device_list.iter(), device_list.len()); + }) + }), + Command::Hap { cmd } => match cmd { + HapSubcommand::Build { + profile: cli::Profile { profile }, + } => with_config(non_interactive, wrapper, |config, _, env| { + ensure_init(config)?; + + hap::cli::build(config, env, noise_level, profile).map_err(Error::HapError) + }), + }, + } + } +} diff --git a/src/open_harmony/config.rs b/src/open_harmony/config.rs new file mode 100644 index 00000000..0526a96d --- /dev/null +++ b/src/open_harmony/config.rs @@ -0,0 +1,228 @@ +use crate::{ + config::app::App, + util::{self, cli::Report}, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fmt::{self, Display}, + path::PathBuf, +}; +use thiserror::Error; + +static DEFAULT_PROJECT_DIR: &str = "gen/ohos"; + +const fn default_true() -> bool { + true +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Metadata { + #[serde(default = "default_true")] + pub supported: bool, + #[serde(default)] + pub no_default_features: bool, + pub cargo_args: Option>, + pub features: Option>, + pub app_sources: Option>, + pub app_plugins: Option>, + pub project_dependencies: Option>, + pub app_dependencies: Option>, + pub app_dependencies_platform: Option>, + pub env_vars: Option>, +} + +impl Default for Metadata { + fn default() -> Self { + Self { + supported: true, + no_default_features: false, + cargo_args: None, + features: None, + app_sources: None, + app_plugins: None, + project_dependencies: None, + app_dependencies: None, + app_dependencies_platform: None, + env_vars: None, + } + } +} + +impl Metadata { + pub const fn supported(&self) -> bool { + self.supported + } + + pub fn no_default_features(&self) -> bool { + self.no_default_features + } + + pub fn cargo_args(&self) -> Option<&[String]> { + self.cargo_args.as_deref() + } + + pub fn features(&self) -> Option<&[String]> { + self.features.as_deref() + } + + pub fn app_sources(&self) -> &[String] { + self.app_sources.as_deref().unwrap_or(&[]) + } + + pub fn app_plugins(&self) -> Option<&[String]> { + self.app_plugins.as_deref() + } + + pub fn project_dependencies(&self) -> Option<&[String]> { + self.project_dependencies.as_deref() + } + + pub fn app_dependencies(&self) -> Option<&[String]> { + self.app_dependencies.as_deref() + } + + pub fn app_dependencies_platform(&self) -> Option<&[String]> { + self.app_dependencies_platform.as_deref() + } +} + +#[derive(Debug)] +pub enum ProjectDirInvalid { + NormalizationFailed { + project_dir: String, + cause: util::NormalizationError, + }, + OutsideOfAppRoot { + project_dir: String, + root_dir: PathBuf, + }, + ContainsSpaces { + project_dir: String, + }, +} + +impl Display for ProjectDirInvalid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NormalizationFailed { project_dir, cause } => { + write!(f, "{:?} couldn't be normalized: {}", project_dir, cause) + } + Self::OutsideOfAppRoot { + project_dir, + root_dir, + } => write!( + f, + "{:?} is outside of the app root {:?}", + project_dir, root_dir, + ), + Self::ContainsSpaces { project_dir } => write!( + f, + "{:?} contains spaces, which the OpenHarmony is remarkably intolerant of", + project_dir + ), + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("open-harmony.project-dir invalid: {0}")] + ProjectDirInvalid(ProjectDirInvalid), + #[error("Identifier cannot contain hyphens on OpenHarmony")] + IdentifierCannotContainHyphens, +} + +impl Error { + pub fn report(&self, msg: &str) -> Report { + Report::error(msg, self) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Raw { + pub project_dir: Option, + pub no_default_features: Option, + pub features: Option>, + #[serde(default)] + pub logcat_filter_specs: Vec, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Config { + #[serde(skip_serializing)] + app: App, + project_dir: PathBuf, + logcat_filter_specs: Vec, +} + +impl Config { + pub fn from_raw(app: App, raw: Option) -> Result { + let raw = raw.unwrap_or_default(); + + if app.identifier().contains('-') { + return Err(Error::IdentifierCannotContainHyphens); + } + + let project_dir = if let Some(project_dir) = raw.project_dir { + if project_dir == DEFAULT_PROJECT_DIR { + log::warn!( + "`{}.project-dir` is set to the default value; you can remove it from your config", + super::NAME + ); + } + if util::under_root(&project_dir, app.root_dir()).map_err(|cause| { + Error::ProjectDirInvalid(ProjectDirInvalid::NormalizationFailed { + project_dir: project_dir.clone(), + cause, + }) + })? { + if !project_dir.contains(' ') { + Ok(project_dir.into()) + } else { + Err(Error::ProjectDirInvalid( + ProjectDirInvalid::ContainsSpaces { project_dir }, + )) + } + } else { + Err(Error::ProjectDirInvalid( + ProjectDirInvalid::OutsideOfAppRoot { + project_dir, + root_dir: app.root_dir().to_owned(), + }, + )) + } + } else { + Ok(DEFAULT_PROJECT_DIR.into()) + }?; + + Ok(Self { + app, + project_dir, + logcat_filter_specs: raw.logcat_filter_specs, + }) + } + + pub fn app(&self) -> &App { + &self.app + } + + pub fn logcat_filter_specs(&self) -> &[String] { + &self.logcat_filter_specs + } + + pub fn so_name(&self) -> String { + format!("lib{}.so", self.app().lib_name()) + } + + pub fn project_dir(&self) -> PathBuf { + self.app.prefix_path(&self.project_dir) + } + + pub fn project_dir_exists(&self) -> bool { + self.project_dir().is_dir() + } +} diff --git a/src/open_harmony/device.rs b/src/open_harmony/device.rs new file mode 100644 index 00000000..2a767161 --- /dev/null +++ b/src/open_harmony/device.rs @@ -0,0 +1,344 @@ +use super::{config::Config, env::Env, target::Target}; +use crate::{ + env::ExplicitEnv, + open_harmony::{hap, hdc}, + opts::{NoiseLevel, Profile}, + util::{ + cli::{Report, Reportable}, + last_modified, + }, + DuctExpressionExt, +}; +use std::fmt::{self, Display}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AabBuildError { + #[error("Failed to build AAB: {0}")] + BuildFailed(std::io::Error), +} + +impl Reportable for AabBuildError { + fn report(&self) -> Report { + match self { + Self::BuildFailed(err) => Report::error("Failed to build AAB", err), + } + } +} + +#[derive(Debug, Error)] +pub enum ApksBuildError { + #[error("Failed to clean old APKS: {0}")] + CleanFailed(std::io::Error), +} + +impl Reportable for ApksBuildError { + fn report(&self) -> Report { + match self { + Self::CleanFailed(err) => Report::error("Failed to clean old APKS", err), + } + } +} + +#[derive(Debug, Error)] +pub enum ApkInstallError { + #[error("Failed to install APK: {0}")] + InstallFailed(#[from] std::io::Error), +} + +impl Reportable for ApkInstallError { + fn report(&self) -> Report { + match self { + Self::InstallFailed(err) => Report::error("Failed to install APK", err), + } + } +} + +#[derive(Debug, Error)] +pub enum RunError { + #[error(transparent)] + HapError(hap::HapError), + #[error(transparent)] + ApkInstallFailed(ApkInstallError), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Reportable for RunError { + fn report(&self) -> Report { + match self { + Self::HapError(err) => err.report(), + Self::ApkInstallFailed(err) => err.report(), + Self::Io(err) => Report::error("IO error", err), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum StacktraceError { + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Reportable for StacktraceError { + fn report(&self) -> Report { + match self { + Self::Io(err) => Report::error("IO error", err), + } + } +} + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Device<'a> { + id: String, + name: String, + model: String, + target: &'a Target<'a>, +} + +impl Display for Device<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + if self.model != self.name { + write!(f, " ({})", self.model)?; + } + Ok(()) + } +} + +impl<'a> Device<'a> { + pub(super) fn new(id: String, name: String, model: String, target: &'a Target<'a>) -> Self { + Self { + id, + name, + model, + target, + } + } + + pub fn target(&self) -> &'a Target<'a> { + self.target + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn model(&self) -> &str { + &self.model + } + + pub fn id(&self) -> &str { + &self.id + } + + fn hdc(&self, env: &Env) -> duct::Expression { + hdc::hdc(env, ["-t", &self.id]) + } + + fn wait_device_boot(&self, env: &Env) { + loop { + let cmd = self + .hdc(env) + .stderr_capture() + .stdout_capture() + .before_spawn(move |cmd| { + cmd.args(["shell", "param", "get", "ohos.boot.time.init"]); + Ok(()) + }); + let handle = cmd.start(); + if let Ok(handle) = handle { + if let Ok(output) = handle.wait() { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + if stdout.trim().parse::().is_ok() { + break; + } + std::thread::sleep(std::time::Duration::from_secs(2)); + } + } else { + break; + } + } + } + } + + fn build_hap( + &self, + config: &Config, + env: &Env, + noise_level: NoiseLevel, + profile: Profile, + ) -> Result<(), hap::HapError> { + hap::build(config, env, noise_level, profile)?; + Ok(()) + } + + fn install_hap(&self, config: &Config, env: &Env) -> Result<(), std::io::Error> { + let hap_path = hap::haps_paths(config) + .into_iter() + .reduce(last_modified) + .unwrap(); + + self.hdc(env) + .before_spawn(move |cmd| { + cmd.args(["install", "-r"]); + cmd.arg(&hap_path); + Ok(()) + }) + .dup_stdio() + .start()? + .wait()?; + + Ok(()) + } + + fn wake_screen(&self, _env: &Env) -> std::io::Result<()> { + // TODO: seems like there's no equivalent to `adb shell input keyevent KEYCODE_WAKEUP` + // in hdc (a keyevent can be sent with `hdc shell uitest uiInput keyEvent Power`) + Ok(()) + } + + // see https://developer.huawei.com/consumer/en/doc/harmonyos-guides/web-debugging-with-devtools + fn setup_devtools_port_forwarding_async(&self, env: &Env, pid: &str) { + let explicit_env = env.explicit_env(); + let hdc_path = env.toolchains_path().join("hdc"); + let pid = pid.to_string(); + + std::thread::spawn(move || { + const MAX_ATTEMPTS: usize = 10; + let mut retries = 0; + + // wait for the remote devtools socket to be opened + let expected_open_socket_content = format!("@webview_devtools_remote_{pid}"); + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + let Ok(opened_sockets) = duct::cmd(&hdc_path, ["shell", "cat", "/proc/net/unix"]) + .vars(explicit_env.clone()) + .read() + else { + break; + }; + if opened_sockets.contains(&expected_open_socket_content) { + break; + } + + retries += 1; + if retries >= MAX_ATTEMPTS { + log::error!( + "Could not setup port forwarding for devtools. Make sure you are running setWebDebuggingAccess(true). See https://developer.huawei.com/consumer/en/doc/harmonyos-guides/web-debugging-with-devtools for more information." + ); + return; + } + } + + // forward the remote devtools socket to the local port 9222 + // so Chrome can connect to it + let _ = duct::cmd( + &hdc_path, + [ + "fport", + "tcp:9222", + &format!("localabstract:webview_devtools_remote_{pid}"), + ], + ) + .vars(explicit_env) + .dup_stdio() + .start(); + }); + } + + #[allow(clippy::too_many_arguments)] + pub fn run( + &self, + config: &Config, + env: &Env, + noise_level: NoiseLevel, + profile: Profile, + ) -> Result { + self.build_hap(config, env, noise_level, profile) + .map_err(RunError::HapError)?; + if self.model.starts_with("emulator") { + self.wait_device_boot(env); + } + self.install_hap(config, env).map_err(RunError::Io)?; + + let identifier = config.app().identifier().to_string(); + self.hdc(env) + .before_spawn(move |cmd| { + cmd.args([ + "shell", + "aa", + "start", + "-b", + &identifier, + "-a", + "EntryAbility", + ]); + Ok(()) + }) + .dup_stdio() + .start()? + .wait()?; + + let _ = self.wake_screen(env); + + let stdout = loop { + let cmd = duct::cmd( + env.toolchains_path().join("hdc"), + ["shell", "pidof", "-s", config.app().identifier()], + ) + .vars(env.explicit_env()) + .stderr_capture() + .stdout_capture(); + let handle = cmd.start()?; + if let Ok(out) = handle.wait() { + if out.status.success() { + break String::from_utf8_lossy(&out.stdout).into_owned(); + } + } + std::thread::sleep(std::time::Duration::from_secs(2)); + }; + let pid = stdout.trim().to_string(); + + self.setup_devtools_port_forwarding_async(env, &pid); + + let mut logcat = duct::cmd(env.toolchains_path().join("hdc"), ["hilog", "-v", "color"]) + .vars(env.explicit_env()) + .dup_stdio(); + + let logcat_filter_specs = config.logcat_filter_specs().to_vec(); + logcat = logcat.before_spawn(move |cmd| { + if !pid.is_empty() { + cmd.args(["--pid", &pid]); + } + cmd.args(&logcat_filter_specs); + Ok(()) + }); + logcat.start().map_err(Into::into) + } + + pub fn stacktrace(&self, config: &Config, env: &Env) -> Result<(), StacktraceError> { + let lib_path = config + .project_dir() + .join("libs") + .join(self.target.arch) + .join(config.app().lib_name()) + .with_extension("so"); + + // -x = print and exit + let hilog_command = hdc::hdc(env, ["-t", &self.id]) + .before_spawn(move |cmd| { + cmd.args(["hilog", "-x"]); + cmd.arg("-sym"); + cmd.arg(&lib_path); + Ok(()) + }) + .dup_stdio(); + + if hilog_command.start()?.wait().is_err() { + println!(" -- no stacktrace --"); + } + Ok(()) + } +} diff --git a/src/open_harmony/emulator/hvd_list.rs b/src/open_harmony/emulator/hvd_list.rs new file mode 100644 index 00000000..a08588db --- /dev/null +++ b/src/open_harmony/emulator/hvd_list.rs @@ -0,0 +1,30 @@ +use super::Emulator; +use std::collections::BTreeSet; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to read HVD list: {0}")] + ReadHvdFailed(std::io::Error), + #[error("Failed to parse HVD list: {0}")] + ParseHvdFailed(serde_json::Error), +} + +pub fn hvd_list() -> Result, Error> { + #[cfg(windows)] + let list_config_path = dirs::data_local_dir() + .unwrap() + .join("Huawei/Emulator/deployed/lists.json"); + #[cfg(not(windows))] + let list_config_path = dirs::home_dir() + .unwrap() + .join(".Huawei/Emulator/deployed/lists.json"); + + if !list_config_path.exists() { + log::error!("HVD list file not found: {}", list_config_path.display()); + return Ok(BTreeSet::new()); + } + + let hvd_json = std::fs::read_to_string(list_config_path).map_err(Error::ReadHvdFailed)?; + serde_json::from_str(&hvd_json).map_err(Error::ParseHvdFailed) +} diff --git a/src/open_harmony/emulator/mod.rs b/src/open_harmony/emulator/mod.rs new file mode 100644 index 00000000..fe124bc5 --- /dev/null +++ b/src/open_harmony/emulator/mod.rs @@ -0,0 +1,71 @@ +mod hvd_list; + +use std::{fmt::Display, path::PathBuf}; + +use duct::Handle; +pub use hvd_list::hvd_list; +use serde::Deserialize; + +use super::env::Env; +use crate::{env::ExplicitEnv, DuctExpressionExt}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +pub struct Emulator { + name: String, + abi: String, + path: PathBuf, +} + +impl Display for Emulator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Emulator { + pub fn name(&self) -> &str { + &self.name + } + + fn command(&self, env: &Env) -> duct::Expression { + let path = self.path.parent().unwrap().to_owned(); + // this is NOT the same as env.ohos_home() + #[cfg(windows)] + let image_root = dirs::data_local_dir().unwrap().join("Huawei").join("Sdk"); + #[cfg(not(windows))] + let image_root = dirs::home_dir() + .unwrap() + .join("Library") + .join("Huawei") + .join("Sdk"); + let emulator_path = if cfg!(target_os = "macos") { + PathBuf::from("/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator") + } else { + std::env::var("DEV_ECO_STUDIO_INSTALL_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("C:\\Program Files\\Huawei\\DevEco Studio")) + .join("tools") + .join("emulator") + .join("Emulator.exe") + }; + duct::cmd(emulator_path, ["-hvd", &self.name]) + .before_spawn(move |cmd| { + cmd.arg("-path") + .arg(&path) + .arg("-imageRoot") + .arg(&image_root); + Ok(()) + }) + .vars(env.explicit_env()) + .dup_stdio() + } + + pub fn start(&self, env: &Env) -> Result { + self.command(env).start() + } + + pub fn start_detached(&self, env: &Env) -> Result<(), std::io::Error> { + self.command(env).run_and_detach()?; + Ok(()) + } +} diff --git a/src/open_harmony/env.rs b/src/open_harmony/env.rs new file mode 100644 index 00000000..16e1e689 --- /dev/null +++ b/src/open_harmony/env.rs @@ -0,0 +1,119 @@ +use crate::{ + env::{Error as CoreError, ExplicitEnv}, + os::Env as CoreEnv, + util::cli::{Report, Reportable}, +}; +use std::{ + collections::HashMap, + ffi::OsString, + path::{Path, PathBuf}, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CoreEnvError(#[from] CoreError), + // TODO: we should be nice and provide a platform-specific suggestion + #[error("Have you installed the OpenHarmony SDK? The `OHOS_HOME` environment variable is set, but doesn't point to an existing directory.")] + OhosHomeNotADir, +} + +impl Reportable for Error { + fn report(&self) -> Report { + match self { + Self::CoreEnvError(err) => err.report(), + _ => Report::error("Failed to initialize OpenHarmony environment", self), + } + } +} + +impl Error { + pub fn sdk_issue(&self) -> bool { + !matches!(self, Self::CoreEnvError(_)) + } +} + +#[derive(Debug, Clone)] +pub struct Env { + pub base: CoreEnv, + ohos_home: PathBuf, +} + +impl Env { + pub fn new() -> Result { + Self::from_env(CoreEnv::new()?) + } + + pub fn from_env(base: CoreEnv) -> Result { + let ohos_home = std::env::var("OHOS_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| { + if cfg!(target_os = "macos") { + PathBuf::from( + "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony", + ) + } else { + std::env::var("DEV_ECO_STUDIO_INSTALL_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| { + PathBuf::from("C:\\Program Files\\Huawei\\DevEco Studio") + }) + .join("sdk") + .join("default") + .join("openharmony") + } + }); + + if ohos_home.is_dir() { + Ok(Self { base, ohos_home }) + } else { + Err(Error::OhosHomeNotADir) + } + } + + pub fn path(&self) -> &OsString { + self.base.path() + } + + pub fn ohos_home(&self) -> &Path { + &self.ohos_home + } + + pub fn toolchains_path(&self) -> PathBuf { + PathBuf::from(&self.ohos_home).join("toolchains") + } +} + +impl ExplicitEnv for Env { + fn explicit_env(&self) -> HashMap { + let mut envs = self.base.explicit_env(); + envs.insert( + "OHOS_HOME".into(), + self.ohos_home.as_os_str().to_os_string(), + ); + envs.insert( + "OHOS_NDK_HOME".into(), + self.ohos_home.as_os_str().to_os_string(), + ); + envs.insert( + "OHOS_BASE_SDK_HOME".into(), + self.ohos_home.as_os_str().to_os_string(), + ); + // seems like only Linux requires this, but let's see + // OHOS_HOME is /path/to/sdk/default/openharmony/18, we want /path/to/sdk for DEVECO_SDK_HOME + envs.insert( + "DEVECO_SDK_HOME".into(), + self.ohos_home + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .as_os_str() + .to_os_string(), + ); + envs + } +} diff --git a/src/open_harmony/hap.rs b/src/open_harmony/hap.rs new file mode 100644 index 00000000..61a0e52e --- /dev/null +++ b/src/open_harmony/hap.rs @@ -0,0 +1,103 @@ +use std::path::PathBuf; + +use colored::Colorize; +use thiserror::Error; + +use super::{config::Config, env::Env}; +use crate::{ + opts::{NoiseLevel, Profile}, + util::{ + cli::{Report, Reportable}, + hvigorw, last_modified, prefix_path, + }, +}; + +#[derive(Debug, Error)] +pub enum HapError { + #[error("Failed to assemble HAP: {0}")] + AssembleFailed(#[from] std::io::Error), +} + +impl Reportable for HapError { + fn report(&self) -> Report { + match self { + Self::AssembleFailed(err) => Report::error("Failed to assemble HAP", err), + } + } +} + +pub fn haps_paths(config: &Config) -> Vec { + let output_dir = prefix_path(config.project_dir(), "entry/build/default/outputs/default"); + vec![ + output_dir.join("entry-default-signed.hap"), + output_dir.join("entry-default-unsigned.hap"), + ] +} + +/// Builds HAP(s) and returns the built HAP(s) paths +pub fn build( + config: &Config, + env: &Env, + noise_level: NoiseLevel, + profile: Profile, +) -> Result, HapError> { + super::ohpm::install(config, env)?; + + let build_mode = profile.as_str().to_lowercase(); + + let hvigor_args = vec![ + "--mode".to_string(), + "module".to_string(), + "assembleHap".to_string(), + "--parallel".to_string(), + "--incremental".to_string(), + "-p".to_string(), + format!("buildMode={build_mode}"), + ]; + + hvigorw(config, env) + .before_spawn(move |cmd| { + cmd.args(&hvigor_args).arg(match noise_level { + NoiseLevel::Polite => "--info", + NoiseLevel::LoudAndProud | NoiseLevel::FranklyQuitePedantic => "--debug", + }); + Ok(()) + }) + .start() + .inspect_err(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + log::error!("`hvigorw` not found. Make sure you have the OpenHarmony command line tools installed and added to your PATH"); + } + })? + .wait()?; + + let mut outputs = Vec::new(); + + let path = haps_paths(config) + .into_iter() + .reduce(last_modified) + .unwrap(); + outputs.push(path); + + Ok(outputs) +} + +pub mod cli { + use super::*; + pub fn build( + config: &Config, + env: &Env, + noise_level: NoiseLevel, + profile: Profile, + ) -> Result<(), HapError> { + println!("Building HAP...\n"); + + let outputs = super::build(config, env, noise_level, profile)?; + + println!("\nFinished building APK(s):"); + for p in &outputs { + println!(" {}", p.to_string_lossy().green(),); + } + Ok(()) + } +} diff --git a/src/open_harmony/hdc/device_list.rs b/src/open_harmony/hdc/device_list.rs new file mode 100644 index 00000000..21818be2 --- /dev/null +++ b/src/open_harmony/hdc/device_list.rs @@ -0,0 +1,69 @@ +use super::get_prop; +use crate::{ + env::ExplicitEnv as _, + open_harmony::{device::Device, env::Env, target::Target}, + util::cli::{Report, Reportable}, +}; +use std::{collections::BTreeSet, process::Command}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to run `hdc devices`: {0}")] + DevicesFailed(#[from] super::RunCheckedError), + #[error(transparent)] + PropFailed(get_prop::Error), + #[error("{0:?} isn't a valid target ABI.")] + AbiInvalid(String), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Reportable for Error { + fn report(&self) -> Report { + let msg = "Failed to detect connected OpenHarmony devices"; + match self { + Self::DevicesFailed(err) => err.report("Failed to run `hdc devices`"), + Self::PropFailed(err) => err.report(), + Self::AbiInvalid(_) => Report::error(msg, self), + Self::Io(err) => Report::error(msg, err), + } + } +} + +pub fn device_list(env: &Env) -> Result>, Error> { + let mut cmd = Command::new(env.toolchains_path().join("hdc")); + cmd.arg("list").arg("targets").envs(env.explicit_env()); + + super::check_authorized(&cmd.output()?) + .map(|raw_list| { + if raw_list.trim() == "[Empty]" { + return Ok(BTreeSet::new()); + } + raw_list + .trim() + .split('\n') + .map(|id| { + let model = + get_prop(env, &id, "const.product.model").map_err(Error::PropFailed)?; + let name = get_prop( + env, + &id, + if model.starts_with("emulator") { + "ohos.qemu.hvd.name" + } else { + "const.product.name" + }, + ) + .unwrap_or_else(|_| id.to_owned()); + let abi = get_prop(env, &id, "const.product.cpu.abilist") + .map_err(Error::PropFailed)?; + let target = + Target::for_abi(&abi).ok_or_else(|| Error::AbiInvalid(abi.clone()))?; + + Ok(Device::new(id.to_owned(), name, model, target)) + }) + .collect() + }) + .map_err(Error::DevicesFailed)? +} diff --git a/src/open_harmony/hdc/get_prop.rs b/src/open_harmony/hdc/get_prop.rs new file mode 100644 index 00000000..c19c5ffb --- /dev/null +++ b/src/open_harmony/hdc/get_prop.rs @@ -0,0 +1,57 @@ +use crate::{ + open_harmony::env::Env, + util::cli::{Report, Reportable}, +}; +use std::str; +use thiserror::Error; + +use super::hdc; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to run `hdc shell getprop {prop}`: {source}")] + LookupFailed { + prop: String, + source: super::RunCheckedError, + }, + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Error { + fn prop(&self) -> &str { + match self { + Self::LookupFailed { prop, .. } => prop, + Self::Io(_) => unreachable!(), + } + } +} + +impl Reportable for Error { + fn report(&self) -> Report { + let msg = format!("Failed to run `hdc shell param get {}`", self.prop()); + match self { + Self::LookupFailed { source, .. } => source.report(&msg), + Self::Io(err) => Report::error("IO error", err), + } + } +} + +pub fn get_prop(env: &Env, serial_no: &str, prop: &str) -> Result { + let prop_ = prop.to_string(); + let handle = hdc(env, ["-t", serial_no]) + .before_spawn(move |cmd| { + cmd.args(["shell", "param", "get", &prop_]); + Ok(()) + }) + .stdin_file(os_pipe::dup_stdin().unwrap()) + .stdout_capture() + .stderr_capture() + .start()?; + + let output = handle.wait()?; + super::check_authorized(output).map_err(|source| Error::LookupFailed { + prop: prop.to_owned(), + source, + }) +} diff --git a/src/open_harmony/hdc/mod.rs b/src/open_harmony/hdc/mod.rs new file mode 100644 index 00000000..81ebcec9 --- /dev/null +++ b/src/open_harmony/hdc/mod.rs @@ -0,0 +1,49 @@ +pub mod device_list; +pub mod get_prop; + +pub use self::{device_list::device_list, get_prop::get_prop}; + +use super::env::Env; +use crate::{env::ExplicitEnv as _, util::cli::Report, DuctExpressionExt}; +use std::{ffi::OsString, str, string::FromUtf8Error}; +use thiserror::Error; + +pub fn hdc(env: &Env, args: U) -> duct::Expression +where + U: IntoIterator, + U::Item: Into, +{ + duct::cmd(env.toolchains_path().join("hdc"), args).vars(env.explicit_env()) +} + +#[derive(Debug, Error)] +pub enum RunCheckedError { + #[error(transparent)] + InvalidUtf8(#[from] FromUtf8Error), + #[error("This device doesn't yet trust this computer. On the device, you should see a prompt like \"Allow USB debugging?\". Pressing \"Allow\" should fix this.")] + Unauthorized, + #[error(transparent)] + CommandFailed(std::io::Error), +} + +impl RunCheckedError { + pub fn report(&self, msg: &str) -> Report { + match self { + Self::InvalidUtf8(err) => Report::error(msg, err), + Self::Unauthorized => Report::action_request(msg, self), + Self::CommandFailed(err) => Report::error(msg, err), + } + } +} + +fn check_authorized(output: &std::process::Output) -> Result { + if !output.status.success() { + if let Ok(stderr) = String::from_utf8(output.stderr.clone()) { + if stderr.contains("error: device unauthorized") { + return Err(RunCheckedError::Unauthorized); + } + } + } + let stdout = String::from_utf8(output.stdout.clone())?.trim().to_string(); + Ok(stdout) +} diff --git a/src/open_harmony/mod.rs b/src/open_harmony/mod.rs new file mode 100644 index 00000000..344de942 --- /dev/null +++ b/src/open_harmony/mod.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "cli")] +pub mod cli; +pub mod config; +pub mod device; +pub mod emulator; +pub mod env; +pub mod hap; +pub mod hdc; +pub mod ohpm; +pub(crate) mod project; +pub mod target; + +pub static NAME: &str = "open-harmony"; diff --git a/src/open_harmony/ohpm.rs b/src/open_harmony/ohpm.rs new file mode 100644 index 00000000..4af77d71 --- /dev/null +++ b/src/open_harmony/ohpm.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +use crate::{ + open_harmony::{config::Config, env::Env}, + DuctExpressionExt, +}; + +pub fn install(config: &Config, env: &Env) -> std::io::Result<()> { + log::info!("Installing packages with ohpm..."); + let mut ohpm_path = if cfg!(target_os = "macos") { + PathBuf::from("/Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm") + } else if cfg!(target_os = "linux") { + // OHOS_HOME is /path/to/sdk/default/openharmony, we want /path/to/ohpm/bin/ohpm so we must call parent() 3 times + env.ohos_home() + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("ohpm") + .join("bin") + .join("ohpm") + } else { + std::env::var("DEV_ECO_STUDIO_INSTALL_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("C:\\Program Files\\Huawei\\DevEco Studio")) + .join("tools") + .join("ohpm") + .join("bin") + .join("ohpm.bat") + }; + if !ohpm_path.exists() { + log::warn!( + "ohpm not found in {}, expecting ohpm to be available in PATH...", + ohpm_path.display() + ); + ohpm_path = PathBuf::from("ohpm"); + } + + duct::cmd(ohpm_path, ["install"]) + .dir(config.project_dir()) + .dup_stdio() + .start()? + .wait()?; + Ok(()) +} diff --git a/src/open_harmony/project.rs b/src/open_harmony/project.rs new file mode 100644 index 00000000..d60ed080 --- /dev/null +++ b/src/open_harmony/project.rs @@ -0,0 +1,171 @@ +use super::{ + config::{Config, Metadata}, + env::Env, + target::Target, +}; +use crate::{ + bicycle, + os::replace_path_separator, + target::TargetTrait as _, + templating::{self, Pack}, + util::{ + self, + cli::{Report, Reportable, TextWrapper}, + ln, + }, +}; +use path_abs::PathOps; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +pub static TEMPLATE_PACK: &str = "dev-eco-studio"; + +#[derive(Debug)] +pub enum Error { + RustupFailed(std::io::Error), + MissingPack(templating::LookupError), + TemplateProcessingFailed(bicycle::ProcessingError), + DirectoryCreationFailed { + path: PathBuf, + cause: std::io::Error, + }, + DirectoryReadFailed { + path: PathBuf, + cause: std::io::Error, + }, + DirectoryRemoveFailed { + path: PathBuf, + cause: std::io::Error, + }, + AssetDirSymlinkFailed(ln::Error), + FileCopyFailed { + src: PathBuf, + dest: PathBuf, + cause: std::io::Error, + }, + AssetSourceInvalid(PathBuf), +} + +impl Reportable for Error { + fn report(&self) -> Report { + match self { + Self::RustupFailed(err) => { + Report::error("Failed to `rustup` OpenHarmony toolchains", err) + } + Self::MissingPack(err) => { + Report::error("Failed to locate OpenHarmony template pack", err) + } + Self::TemplateProcessingFailed(err) => { + Report::error("OpenHarmony template processing failed", err) + } + Self::DirectoryCreationFailed { path, cause } => Report::error( + format!( + "Failed to create OpenHarmony assets directory at {:?}", + path + ), + cause, + ), + Self::DirectoryReadFailed { path, cause } => { + Report::error(format!("Failed to read directory at {:?}", path), cause) + } + Self::DirectoryRemoveFailed { path, cause } => Report::error( + format!("Failed to remove directory directory at {:?}", path), + cause, + ), + Self::AssetDirSymlinkFailed(err) => Report::error( + "Asset dir couldn't be symlinked into OpenHarmony project", + err, + ), + Self::FileCopyFailed { src, dest, cause } => Report::error( + format!("Failed to copy file at {:?} to {:?}", src, dest), + cause, + ), + Self::AssetSourceInvalid(src) => Report::error( + format!("Asset source at {:?} invalid", src), + "Asset sources must be either a directory or a file", + ), + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn gen( + config: &Config, + metadata: &Metadata, + _env: &Env, + bike: &bicycle::Bicycle, + _wrapper: &TextWrapper, + filter: &templating::Filter, + skip_targets_install: bool, +) -> Result<(), Error> { + if !skip_targets_install { + println!("Installing OpenHarmony toolchains..."); + Target::install_all().map_err(Error::RustupFailed)?; + } + println!("Generating DevEco Studio project..."); + let src = Pack::lookup_platform(TEMPLATE_PACK) + .map_err(Error::MissingPack)? + .expect_local(); + let dest = config.project_dir(); + + bike.filter_and_process( + src, + &dest, + |map| { + map.insert( + "root-dir-rel", + Path::new(&replace_path_separator( + util::relativize_path( + config.app().root_dir(), + config.project_dir().join("entry"), + ) + .into_os_string(), + )), + ); + map.insert("root-dir", config.app().root_dir()); + map.insert( + "abi-list", + Target::all() + .values() + .map(|target| target.abi) + .collect::>(), + ); + map.insert("target-list", Target::all().keys().collect::>()); + map.insert( + "arch-list", + Target::all() + .values() + .map(|target| target.arch) + .collect::>(), + ); + map.insert( + "has-code", + metadata.project_dependencies().is_some() + || metadata.app_dependencies().is_some() + || metadata.app_dependencies_platform().is_some(), + ); + map.insert("windows", cfg!(windows)); + }, + filter.fun(), + ) + .map_err(Error::TemplateProcessingFailed)?; + + let source_dest = dest.join("app"); + for source in metadata.app_sources() { + let source_src = config.app().root_dir().join(source); + let source_file = source_src + .file_name() + .ok_or_else(|| Error::AssetSourceInvalid(source_src.clone()))?; + fs::copy(&source_src, source_dest.join(source_file)).map_err(|cause| { + Error::FileCopyFailed { + src: source_src, + dest: source_dest.clone(), + cause, + } + })?; + } + + Ok(()) +} diff --git a/src/open_harmony/target.rs b/src/open_harmony/target.rs new file mode 100644 index 00000000..4b2e502a --- /dev/null +++ b/src/open_harmony/target.rs @@ -0,0 +1,252 @@ +use super::{ + config::{Config, Metadata}, + env::Env, +}; +use crate::{ + env::ExplicitEnv, + opts::{NoiseLevel, Profile}, + target::TargetTrait, + util::cli::{Report, Reportable}, + DuctExpressionExt, +}; +use once_cell_regex::exports::once_cell::sync::OnceCell; +use serde::Serialize; +use std::{collections::BTreeMap, fmt, io, path::PathBuf, str}; +use thiserror::Error; + +#[derive(Clone, Copy, Debug)] +pub enum CargoMode { + Check, + Build, +} + +impl fmt::Display for CargoMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CargoMode::Check => write!(f, "check"), + CargoMode::Build => write!(f, "build"), + } + } +} + +impl CargoMode { + pub fn as_str(&self) -> &'static str { + match self { + CargoMode::Check => "check", + CargoMode::Build => "build", + } + } +} + +#[derive(Debug, Error)] +pub enum CompileLibError { + #[error("`Failed to run `ohrs {mode}`: {cause}")] + OhrsFailed { + mode: CargoMode, + cause: std::io::Error, + }, + #[error("`Failed to write file at {path} : {cause}")] + FileWrite { path: PathBuf, cause: io::Error }, +} + +impl Reportable for CompileLibError { + fn report(&self) -> Report { + Report::error("Failed to compile lib", self) + } +} + +#[derive(Debug, Error)] +pub enum SymlinkLibsError { + #[error("Failed to create \"jniLibs\" directory: {0}")] + JniLibsCreationFailed(io::Error), + #[error("Library artifact not found at {path}. Make sure your Cargo.toml file has a [lib] block with `crate-type = [\"staticlib\", \"cdylib\", \"rlib\"]`")] + LibNotFound { path: PathBuf }, +} + +impl Reportable for SymlinkLibsError { + fn report(&self) -> Report { + Report::error("Failed to symlink lib", self) + } +} + +#[derive(Debug, Error)] +pub enum BuildError { + #[error(transparent)] + BuildFailed(CompileLibError), + #[error(transparent)] + SymlinkLibsFailed(SymlinkLibsError), +} + +impl Reportable for BuildError { + fn report(&self) -> Report { + match self { + Self::BuildFailed(err) => err.report(), + Self::SymlinkLibsFailed(err) => err.report(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Target<'a> { + pub triple: &'a str, + pub abi: &'a str, + pub arch: &'a str, +} + +impl<'a> TargetTrait<'a> for Target<'a> { + const DEFAULT_KEY: &'static str = "aarch64"; + + fn all() -> &'a BTreeMap<&'a str, Self> { + static TARGETS: OnceCell>> = OnceCell::new(); + TARGETS.get_or_init(|| { + let mut targets = BTreeMap::new(); + targets.insert( + "aarch64", + Target { + triple: "aarch64-unknown-linux-ohos", + abi: "arm64-v8a", + arch: "arm64", + }, + ); + targets.insert( + "armv7", + Target { + triple: "aarch64-unknown-linux-ohos", + abi: "armeabi-v7a", + arch: "arm", + }, + ); + targets.insert( + "x86_64", + Target { + triple: "x86_64-unknown-linux-ohos", + abi: "x86_64", + arch: "x86_64", + }, + ); + targets + }) + } + + fn name_list() -> Vec<&'a str> { + Self::all().keys().copied().collect::>() + } + + fn triple(&'a self) -> &'a str { + self.triple + } + + fn arch(&'a self) -> &'a str { + self.arch + } +} + +impl<'a> Target<'a> { + pub fn for_abi(abi: &str) -> Option<&'a Self> { + Self::all().values().find(|target| target.abi == abi) + } + + pub fn arch_upper_camel_case(&'a self) -> &'a str { + match self.arch() { + "arm" => "Arm", + "arm64" => "Arm64", + "x86_64" => "X86_64", + "x86" => "X86", + arch => arch, + } + } + + #[allow(clippy::too_many_arguments)] + fn compile_lib( + &self, + config: &Config, + metadata: &Metadata, + env: &Env, + noise_level: NoiseLevel, + force_color: bool, + profile: Profile, + mode: CargoMode, + ) -> Result<(), CompileLibError> { + // Force color, since gradle would otherwise give us uncolored output + // (which Android Studio makes red, which is extra gross!) + let color = if force_color { "always" } else { "auto" }; + + let mut cargo_args: Vec = vec![ + "--package".into(), + config.app().name().into(), + "--manifest-path".into(), + config.app().manifest_path().to_str().unwrap().into(), + "--color".into(), + color.into(), + ]; + if noise_level.pedantic() { + cargo_args.push("-vv".into()); + } + if let Some(args) = metadata.cargo_args() { + cargo_args.extend_from_slice(args); + } + if let Some(features) = metadata.features() { + let features = features.join(" "); + cargo_args.extend_from_slice(&["--features".into(), features.as_str().to_string()]); + } + if profile.release() { + cargo_args.push("--release".into()); + } + if metadata.no_default_features() { + cargo_args.push("--no-default-features".into()); + } + + let dist = config.project_dir().join("entry").join("libs"); + + duct::cmd("ohrs", ["build", "--arch", self.arch]) + .before_spawn(move |cmd| { + cmd.arg("--dist").arg(&dist).arg("--").args(&cargo_args); + Ok(()) + }) + .vars(env.explicit_env()) + .run() + .map_err(|cause| CompileLibError::OhrsFailed { mode, cause })?; + + Ok(()) + } + + pub fn check( + &self, + config: &Config, + metadata: &Metadata, + env: &Env, + noise_level: NoiseLevel, + force_color: bool, + ) -> Result<(), CompileLibError> { + self.compile_lib( + config, + metadata, + env, + noise_level, + force_color, + Profile::Debug, + CargoMode::Check, + ) + } + + pub fn build( + &self, + config: &Config, + metadata: &Metadata, + env: &Env, + noise_level: NoiseLevel, + force_color: bool, + profile: Profile, + ) -> Result<(), BuildError> { + self.compile_lib( + config, + metadata, + env, + noise_level, + force_color, + profile, + CargoMode::Build, + ) + .map_err(BuildError::BuildFailed) + } +} diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs index 4120af02..376191e1 100644 --- a/src/os/windows/mod.rs +++ b/src/os/windows/mod.rs @@ -154,6 +154,7 @@ pub fn open_file_with( // 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, env), + Some("DevEco-Studio") => open_file_with_deveco_studio(path, env), _ => { unimplemented!() } @@ -164,9 +165,14 @@ const ANDROID_STUDIO_UNINSTALL_KEY_PATH: PCWSTR = w!("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Android Studio"); const ANDROID_STUDIO_UNINSTALLER_VALUE: PCWSTR = w!("UninstallString"); #[cfg(target_pointer_width = "64")] -const STUDIO_EXE_PATH: &str = "bin/studio64.exe"; +const ANDROID_STUDIO_EXE_PATH: &str = "bin/studio64.exe"; #[cfg(target_pointer_width = "32")] -const STUDIO_EXE_PATH: &str = "bin/studio.exe"; +const ANDROID_STUDIO_EXE_PATH: &str = "bin/studio.exe"; + +// DevEco is 64-bit only but registers itself in WOW6432Node for some reason. +const DEVECO_STUDIO_UNINSTALL_KEY_PATH: PCWSTR = + w!("SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\DevEco Studio"); +const DEVECO_STUDIO_DISPLAYICON_VALUE: PCWSTR = w!("DisplayIcon"); fn open_file_with_android_studio(path: impl AsRef, env: &Env) -> Result<(), OpenFileError> { let mut application_path = which("studio.cmd").unwrap_or_default(); @@ -188,7 +194,39 @@ fn open_file_with_android_studio(path: impl AsRef, env: &Env) -> Result<( application_path = Path::new(&uninstaller_path) .parent() .expect("Failed to get Android Studio uninstaller's parent path") - .join(STUDIO_EXE_PATH); + .join(ANDROID_STUDIO_EXE_PATH); + } + duct::cmd( + application_path, + [ + dunce::canonicalize(Path::new(path.as_ref())) + .expect("Failed to canonicalize file path"), + ], + ) + .vars(env.explicit_env()) + .run_and_detach() + .map_err(OpenFileError::LaunchFailed)?; + Ok(()) +} + +fn open_file_with_deveco_studio(path: impl AsRef, env: &Env) -> Result<(), OpenFileError> { + let mut application_path = which("devecostudio.bat").unwrap_or_default(); + if !application_path.is_file() { + let mut buffer = [0; MAX_PATH as usize]; + unsafe { + SHRegGetPathW( + HKEY_LOCAL_MACHINE, + PCWSTR::from_raw(DEVECO_STUDIO_UNINSTALL_KEY_PATH.as_ptr()), + PCWSTR::from_raw(DEVECO_STUDIO_DISPLAYICON_VALUE.as_ptr()), + &mut buffer, + 0, + ) + .ok() + .map_err(|e| OpenFileError::IOError(e.into()))? + }; + let len = NullTerminatedWTF16Iterator(buffer.as_ptr()).count(); + let displayicon_path = OsString::from_wide(&buffer[..len]); + application_path = std::path::PathBuf::from(&displayicon_path); } duct::cmd( application_path, diff --git a/src/util/mod.rs b/src/util/mod.rs index 05652470..72783096 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -691,3 +691,49 @@ pub fn gradlew( .dup_stdio() } } + +pub fn hvigorw( + config: &crate::open_harmony::config::Config, + env: &crate::open_harmony::env::Env, +) -> duct::Expression { + let project_dir = config.project_dir(); + #[cfg(windows)] + let hvigorw = "hvigorw.bat"; + #[cfg(not(windows))] + let hvigorw = "hvigorw"; + + let hvigorw_exists = which::which(hvigorw).is_ok(); + + let project_dir = dunce::simplified(&project_dir); + // note: DevEco Studio is not supported on Linux yet, so we rely on hvigorw + if hvigorw_exists || cfg!(target_os = "linux") { + duct::cmd::<&str, [String; 0]>(hvigorw, []) + .dir(project_dir) + .vars(env.explicit_env()) + .dup_stdio() + } else { + let (node_path, hvigorw_script_path, deveco_sdk_home) = if cfg!(target_os = "macos") { + ( + PathBuf::from("/Applications/DevEco-Studio.app/Contents/tools/node/bin/node"), + PathBuf::from( + "/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js", + ), + PathBuf::from("/Applications/DevEco-Studio.app/Contents/sdk"), + ) + } else { + let dev_eco_studio_install_path = std::env::var("DEV_ECO_STUDIO_INSTALL_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("C:\\Program Files\\Huawei\\DevEco Studio")); + ( + dev_eco_studio_install_path.join("tools/node/node.exe"), + dev_eco_studio_install_path.join("tools/hvigor/bin/hvigorw.js"), + dev_eco_studio_install_path.join("sdk"), + ) + }; + duct::cmd(node_path, [hvigorw_script_path]) + .dir(project_dir) + .vars(env.explicit_env()) + .env("DEVECO_SDK_HOME", deveco_sdk_home) + .dup_stdio() + } +} diff --git a/templates/platforms/dev-eco-studio/.gitignore b/templates/platforms/dev-eco-studio/.gitignore new file mode 100644 index 00000000..d2ff2014 --- /dev/null +++ b/templates/platforms/dev-eco-studio/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/AppScope/app.json5 b/templates/platforms/dev-eco-studio/AppScope/app.json5 new file mode 100644 index 00000000..86053732 --- /dev/null +++ b/templates/platforms/dev-eco-studio/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "{{app.identifier}}", + "vendor": "{{app.publisher}}", + "versionCode": 1, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/templates/platforms/dev-eco-studio/AppScope/resources/base/element/string.json b/templates/platforms/dev-eco-studio/AppScope/resources/base/element/string.json new file mode 100644 index 00000000..d0d961de --- /dev/null +++ b/templates/platforms/dev-eco-studio/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "{{app.stylized-name}}" + } + ] +} diff --git a/templates/platforms/dev-eco-studio/AppScope/resources/base/media/background.png b/templates/platforms/dev-eco-studio/AppScope/resources/base/media/background.png new file mode 100644 index 00000000..923f2b3f Binary files /dev/null and b/templates/platforms/dev-eco-studio/AppScope/resources/base/media/background.png differ diff --git a/templates/platforms/dev-eco-studio/AppScope/resources/base/media/foreground.png b/templates/platforms/dev-eco-studio/AppScope/resources/base/media/foreground.png new file mode 100644 index 00000000..97014d3e Binary files /dev/null and b/templates/platforms/dev-eco-studio/AppScope/resources/base/media/foreground.png differ diff --git a/templates/platforms/dev-eco-studio/AppScope/resources/base/media/layered_image.json b/templates/platforms/dev-eco-studio/AppScope/resources/base/media/layered_image.json new file mode 100644 index 00000000..71b57c97 --- /dev/null +++ b/templates/platforms/dev-eco-studio/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} diff --git a/templates/platforms/dev-eco-studio/build-profile.json5 b/templates/platforms/dev-eco-studio/build-profile.json5 new file mode 100644 index 00000000..1e69556b --- /dev/null +++ b/templates/platforms/dev-eco-studio/build-profile.json5 @@ -0,0 +1,41 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.0.0(12)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/code-linter.json5 b/templates/platforms/dev-eco-studio/code-linter.json5 new file mode 100644 index 00000000..073990fa --- /dev/null +++ b/templates/platforms/dev-eco-studio/code-linter.json5 @@ -0,0 +1,32 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/.gitignore b/templates/platforms/dev-eco-studio/entry/.gitignore new file mode 100644 index 00000000..6e08acb4 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test +/libs diff --git a/templates/platforms/dev-eco-studio/entry/build-profile.json5 b/templates/platforms/dev-eco-studio/entry/build-profile.json5 new file mode 100644 index 00000000..38bdcc99 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/build-profile.json5 @@ -0,0 +1,39 @@ +{ + "apiType": "stageMode", + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "", + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + }, + "nativeLib": { + "debugSymbol": { + "strip": true, + "exclude": [] + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/hvigorfile.ts b/templates/platforms/dev-eco-studio/entry/hvigorfile.ts new file mode 100644 index 00000000..51c3006c --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/hvigorfile.ts @@ -0,0 +1,29 @@ +import { hapTasks } from "@ohos/hvigor-ohos-plugin"; +import { hvigor, HvigorPlugin, HvigorNode, HvigorTask } from "@ohos/hvigor"; +import { execFileSync } from "child_process"; +import { resolve } from "path"; + +export default { + system: hapTasks /* Built-in plugin of Hvigor. It cannot be modified. */, + plugins: [ + cargoMobilelugin(), + ] /* Custom plugin to extend the functionality of Hvigor. */, +}; + +function cargoMobilelugin(): HvigorPlugin { + return { + pluginId: "tauri", + apply(node: HvigorNode) { + const buildRustCode = () => { + const properties = hvigor.getParameter().getProperties(); + const target = properties.target || "aarch64"; + execFileSync(`cargo`, ["open-harmony", "build", "--target", target], { + cwd: resolve(__dirname, "{{root-dir-rel}}"), + stdio: "inherit", + }); + }; + + node.getTaskByName("default@ConfigureCmake").afterRun(buildRustCode); + }, + }; +} diff --git a/templates/platforms/dev-eco-studio/entry/obfuscation-rules.txt b/templates/platforms/dev-eco-studio/entry/obfuscation-rules.txt new file mode 100644 index 00000000..272efb6c --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/oh-package-lock.json5 b/templates/platforms/dev-eco-studio/entry/oh-package-lock.json5 new file mode 100644 index 00000000..f057eba5 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/oh-package-lock.json5 @@ -0,0 +1,27 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos-rs/ability@0.2.1": "@ohos-rs/ability@0.2.1", + "libentry.so@src/main/cpp/types/libentry": "libentry.so@src/main/cpp/types/libentry" + }, + "packages": { + "@ohos-rs/ability@0.2.1": { + "name": "@ohos-rs/ability", + "version": "0.2.1", + "integrity": "sha512-/IYR/8+TgAn5ij3OT/gRk4e71eQ/+WBFQuADqMRyIUhIEVg/9MhDyWZJmITvm50ay5cMbGtry14b+TN0vwlOBg==", + "resolved": "https://repo.harmonyos.com/ohpm/@ohos-rs/ability/-/ability-0.2.1.har", + "registryType": "ohpm" + }, + "libentry.so@src/main/cpp/types/libentry": { + "name": "libentry.so", + "version": "1.0.0", + "resolved": "src/main/cpp/types/libentry", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/oh-package.json5 b/templates/platforms/dev-eco-studio/entry/oh-package.json5 new file mode 100644 index 00000000..6a9d51cc --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/oh-package.json5 @@ -0,0 +1,12 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "libentry.so": "file:./src/main/cpp/types/libentry", + "@ohos-rs/ability": "0.2.1" + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/cpp/CMakeLists.txt b/templates/platforms/dev-eco-studio/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..0026865c --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,15 @@ +# the minimum version of CMake. +cmake_minimum_required(VERSION 3.5.0) +project(webview_example) + +set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +if(DEFINED PACKAGE_FIND_FILE) + include(${PACKAGE_FIND_FILE}) +endif() + +include_directories(${NATIVERENDER_ROOT_PATH} + ${NATIVERENDER_ROOT_PATH}/include) + +add_library(entry SHARED napi_init.cpp) +target_link_libraries(entry PUBLIC libace_napi.z.so) \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/cpp/napi_init.cpp b/templates/platforms/dev-eco-studio/entry/src/main/cpp/napi_init.cpp new file mode 100644 index 00000000..85330e3e --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/cpp/napi_init.cpp @@ -0,0 +1,53 @@ +#include "napi/native_api.h" + +static napi_value Add(napi_env env, napi_callback_info info) +{ + size_t argc = 2; + napi_value args[2] = {nullptr}; + + napi_get_cb_info(env, info, &argc, args , nullptr, nullptr); + + napi_valuetype valuetype0; + napi_typeof(env, args[0], &valuetype0); + + napi_valuetype valuetype1; + napi_typeof(env, args[1], &valuetype1); + + double value0; + napi_get_value_double(env, args[0], &value0); + + double value1; + napi_get_value_double(env, args[1], &value1); + + napi_value sum; + napi_create_double(env, value0 + value1, &sum); + + return sum; + +} + +EXTERN_C_START +static napi_value Init(napi_env env, napi_value exports) +{ + napi_property_descriptor desc[] = { + { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr } + }; + napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); + return exports; +} +EXTERN_C_END + +static napi_module demoModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, + .nm_modname = "entry", + .nm_priv = ((void*)0), + .reserved = { 0 }, +}; + +extern "C" __attribute__((constructor)) void RegisterEntryModule(void) +{ + napi_module_register(&demoModule); +} diff --git a/templates/platforms/dev-eco-studio/entry/src/main/cpp/types/libentry/Index.d.ts b/templates/platforms/dev-eco-studio/entry/src/main/cpp/types/libentry/Index.d.ts new file mode 100644 index 00000000..e44f3615 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/cpp/types/libentry/Index.d.ts @@ -0,0 +1 @@ +export const add: (a: number, b: number) => number; \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/cpp/types/libentry/oh-package.json5 b/templates/platforms/dev-eco-studio/entry/src/main/cpp/types/libentry/oh-package.json5 new file mode 100644 index 00000000..ea410725 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/cpp/types/libentry/oh-package.json5 @@ -0,0 +1,6 @@ +{ + "name": "libentry.so", + "types": "./Index.d.ts", + "version": "1.0.0", + "description": "Please describe the basic information." +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/ets/entryability/EntryAbility.ets b/templates/platforms/dev-eco-studio/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 00000000..b5608406 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,21 @@ +import { RustAbility } from '@ohos-rs/ability' +import Want from '@ohos.app.ability.Want' +import { AbilityConstant } from '@kit.AbilityKit'; +import window from '@ohos.window'; + +export default class EntryAbility extends RustAbility { + // change to dynamic library name + public moduleName: string = "{{app.name}}" + public defaultPage: boolean = true; + public mode: 'xcomponent' | 'webview' = 'webview' + + async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { + super.onCreate(want, launchParam); + } + + async onWindowStageCreate(windowStage: window.WindowStage): Promise { + const window = windowStage.getMainWindowSync(); + await window.setWindowLayoutFullScreen(false); + super.onWindowStageCreate(windowStage); + } +} diff --git a/templates/platforms/dev-eco-studio/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/templates/platforms/dev-eco-studio/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 00000000..8e4de992 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,16 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/ets/pages/Index.ets b/templates/platforms/dev-eco-studio/entry/src/main/ets/pages/Index.ets new file mode 100644 index 00000000..0738f879 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,26 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import testNapi from 'libentry.so'; + +const DOMAIN = 0x0000; + +@Entry +@Component +struct Index { + @State message: string = 'Hello World'; + + build() { + Row() { + Column() { + Text(this.message) + .fontSize($r('app.float.page_text_font_size')) + .fontWeight(FontWeight.Bold) + .onClick(() => { + this.message = 'Welcome'; + hilog.info(DOMAIN, 'testTag', 'Test NAPI 2 + 3 = %{public}d', testNapi.add(2, 3)); + }) + } + .width('100%') + } + .height('100%') + } +} diff --git a/templates/platforms/dev-eco-studio/entry/src/main/module.json5 b/templates/platforms/dev-eco-studio/entry/src/main/module.json5 new file mode 100644 index 00000000..59b6e83a --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/module.json5 @@ -0,0 +1,57 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + } + ] + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/color.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/color.json new file mode 100644 index 00000000..3c712962 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/float.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/float.json new file mode 100644 index 00000000..33ea2230 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/string.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/string.json new file mode 100644 index 00000000..f9459551 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + } + ] +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/background.png b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/background.png new file mode 100644 index 00000000..923f2b3f Binary files /dev/null and b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/background.png differ diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/foreground.png b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 00000000..97014d3e Binary files /dev/null and b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/foreground.png differ diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/layered_image.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 00000000..fb499204 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/startIcon.png b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 00000000..205ad8b5 Binary files /dev/null and b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/media/startIcon.png differ diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/profile/backup_config.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 00000000..78f40ae7 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/base/profile/main_pages.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 00000000..1898d94f --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/templates/platforms/dev-eco-studio/entry/src/main/resources/dark/element/color.json b/templates/platforms/dev-eco-studio/entry/src/main/resources/dark/element/color.json new file mode 100644 index 00000000..79b11c27 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/mock/Libentry.mock.ets b/templates/platforms/dev-eco-studio/entry/src/mock/Libentry.mock.ets new file mode 100644 index 00000000..c2171716 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/mock/Libentry.mock.ets @@ -0,0 +1,7 @@ +const NativeMock: Record = { + 'add': (a: number, b: number) => { + return a + b; + }, +}; + +export default NativeMock; \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/mock/mock-config.json5 b/templates/platforms/dev-eco-studio/entry/src/mock/mock-config.json5 new file mode 100644 index 00000000..6540976c --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/mock/mock-config.json5 @@ -0,0 +1,5 @@ +{ + "libentry.so": { + "source": "src/mock/Libentry.mock.ets" + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/ohosTest/ets/test/Ability.test.ets b/templates/platforms/dev-eco-studio/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 00000000..85c78f67 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/ohosTest/ets/test/List.test.ets b/templates/platforms/dev-eco-studio/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 00000000..794c7dc4 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/ohosTest/module.json5 b/templates/platforms/dev-eco-studio/entry/src/ohosTest/module.json5 new file mode 100644 index 00000000..55725a92 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/ohosTest/module.json5 @@ -0,0 +1,13 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/templates/platforms/dev-eco-studio/entry/src/test/List.test.ets b/templates/platforms/dev-eco-studio/entry/src/test/List.test.ets new file mode 100644 index 00000000..bb5b5c37 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/entry/src/test/LocalUnit.test.ets b/templates/platforms/dev-eco-studio/entry/src/test/LocalUnit.test.ets new file mode 100644 index 00000000..165fc161 --- /dev/null +++ b/templates/platforms/dev-eco-studio/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/hvigor/hvigor-config.json5 b/templates/platforms/dev-eco-studio/hvigor/hvigor-config.json5 new file mode 100644 index 00000000..15688394 --- /dev/null +++ b/templates/platforms/dev-eco-studio/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.3", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/templates/platforms/dev-eco-studio/hvigorfile.ts b/templates/platforms/dev-eco-studio/hvigorfile.ts new file mode 100644 index 00000000..9d563d9c --- /dev/null +++ b/templates/platforms/dev-eco-studio/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from "@ohos/hvigor-ohos-plugin"; + +export default { + system: appTasks /* Built-in plugin of Hvigor. It cannot be modified. */, + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */, +}; diff --git a/templates/platforms/dev-eco-studio/oh-package-lock.json5 b/templates/platforms/dev-eco-studio/oh-package-lock.json5 new file mode 100644 index 00000000..a3925056 --- /dev/null +++ b/templates/platforms/dev-eco-studio/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.21": "@ohos/hypium@1.0.21" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.21": { + "name": "@ohos/hypium", + "version": "1.0.21", + "integrity": "sha512-iyKGMXxE+9PpCkqEwu0VykN/7hNpb+QOeIuHwkmZnxOpI+dFZt6yhPB7k89EgV1MiSK/ieV/hMjr5Z2mWwRfMQ==", + "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hypium/-/hypium-1.0.21.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/templates/platforms/dev-eco-studio/oh-package.json5 b/templates/platforms/dev-eco-studio/oh-package.json5 new file mode 100644 index 00000000..c44d132b --- /dev/null +++ b/templates/platforms/dev-eco-studio/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "5.0.3", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.21", + "@ohos/hamock": "1.0.0" + } +}