Skip to content

Commit

Permalink
Break up the ubrn command line build commands (#71)
Browse files Browse the repository at this point in the history
According to [The Big O of Code
Reviews](https://www.egorand.dev/the-big-o-of-code-reviews/), this is a
O(_n_) change.

This adds a number of suppression flags to the command line. This is to
make it:

- easier to generate code using your own build process
- easier to customize parts of the build process
- cache as much as possible of the previous build

For `build ios`:

- `--no-xcodebuild` suppress the creation of an xcframework. The
xcframework is expected, but with this flag, you are expected to provide
it; perhaps after generating swift from a library file. (future work
idea: add an arg to generate Swift from uniffi).
- `--no-cargo` detects if any targets are already built, and uses that
to generate library files. If no targets are built, then build all,
re-using the library files next time.

For `build android`

- `--no-jniLibs`, this is Android reflection of `--no-xcodebuild`. I'm
not sure what this would be used for.
- `--no-cargo` detects if any targets are already built, and uses that
to generate library files. If no targets are built, then build all,
re-using the library files next time.

A new command: `generate all --config config.yaml lib.a`:

- This combines `generate bindings` and `generate turbo-module` into a
higher level command which accepts a config file and a pre-built library
file.
  • Loading branch information
jhugman authored Aug 18, 2024
1 parent 5109542 commit 90c9e92
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 119 deletions.
170 changes: 131 additions & 39 deletions crates/ubrn_cli/src/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/
*/
use serde::Deserialize;
use std::{fmt::Display, fs, process::Command, str::FromStr};
use std::{collections::HashMap, fmt::Display, fs, process::Command, str::FromStr};

use clap::Args;

Expand All @@ -15,6 +15,7 @@ use ubrn_common::{mk_dir, rm_dir, run_cmd, CrateMetadata};
use crate::{
building::{CommonBuildArgs, ExtraArgs},
config::ProjectConfig,
rust::CrateConfig,
workspace,
};

Expand Down Expand Up @@ -115,68 +116,159 @@ pub(crate) struct AndroidArgs {

#[clap(flatten)]
pub(crate) common_args: CommonBuildArgs,

/// Suppress the copying of the Rust library into the JNI library directories.
#[clap(long = "no-jniLibs")]
no_jni_libs: bool,
}

impl AndroidArgs {
pub(crate) fn build(&self) -> Result<Vec<Utf8PathBuf>> {
let config: ProjectConfig = self.project_config()?;
let project_root = config.project_root();

let crate_ = &config.crate_;
let rust_dir = crate_.directory()?;
let manifest_path = crate_.manifest_path()?;

let android = &config.android;

let jni_libs = android.jni_libs(project_root);
rm_dir(&jni_libs)?;

let mut target_files: Vec<_> = Vec::new();
for target in &android.targets {
let target = target.parse::<Target>()?;
let mut cmd = Command::new("cargo");
cmd.arg("ndk")
.arg("--manifest-path")
.arg(&manifest_path)
.arg("--target")
.arg(target.to_string())
.arg("--platform")
.arg(format!("{}", android.api_level));

if !self.common_args.release {
cmd.arg("--no-strip");
let target_files = if self.common_args.no_cargo {
let files = self.find_existing(&crate_.metadata()?, &android.targets);
if !files.is_empty() {
files
} else {
self.cargo_build_all(
crate_,
&android.targets,
&android.cargo_extras,
android.api_level,
)?
}
} else {
self.cargo_build_all(
crate_,
&android.targets,
&android.cargo_extras,
android.api_level,
)?
};

if !self.no_jni_libs {
let project_root = config.project_root();
self.copy_into_jni_libs(
&crate_.metadata()?,
&android.jni_libs(project_root),
&target_files,
)?;
}

cmd.arg("--").arg("build");
if self.common_args.release {
cmd.arg("--release");
}
Ok(target_files.into_values().collect())
}

fn cargo_build_all(
&self,
crate_: &CrateConfig,
targets: &[String],
cargo_extras: &ExtraArgs,
api_level: usize,
) -> Result<HashMap<Target, Utf8PathBuf>> {
let rust_dir = crate_.directory()?;
let manifest_path = crate_.manifest_path()?;
let metadata = crate_.metadata()?;
let mut target_files = HashMap::new();
let profile = self.common_args.profile();
for target in targets {
let target =
self.cargo_build(target, &manifest_path, cargo_extras, api_level, &rust_dir)?;
let library = metadata.library_path(Some(target.triple()), profile);
metadata.library_path_exists(&library)?;
target_files.insert(target, library);
}
Ok(target_files)
}

cmd.args(android.cargo_extras.clone());
fn cargo_build(
&self,
target: &str,
manifest_path: &Utf8PathBuf,
cargo_extras: &ExtraArgs,
api_level: usize,
rust_dir: &Utf8PathBuf,
) -> Result<Target> {
let target = target.parse::<Target>()?;
let mut cmd = Command::new("cargo");
cmd.arg("ndk")
.arg("--manifest-path")
.arg(manifest_path)
.arg("--target")
.arg(target.to_string())
.arg("--platform")
.arg(format!("{}", api_level));
if !self.common_args.release {
cmd.arg("--no-strip");
}
cmd.arg("--").arg("build");
if self.common_args.release {
cmd.arg("--release");
}
cmd.args(cargo_extras.clone());
run_cmd(cmd.current_dir(rust_dir))?;
Ok(target)
}

run_cmd(cmd.current_dir(&rust_dir))?;
let metadata = crate_.metadata()?;
let src_lib = metadata.library_path(
Some(target.triple()),
CrateMetadata::profile(self.common_args.release),
)?;
fn find_existing(
&self,
metadata: &CrateMetadata,
targets: &[String],
) -> HashMap<Target, Utf8PathBuf> {
let profile = self.common_args.profile();
targets
.iter()
.filter_map(|target| {
let target = target.parse::<Target>();
match target {
Ok(target) => Some(target),
Err(_) => None,
}
})
.filter_map(|target| {
let library = metadata.library_path(Some(target.triple()), profile);
if library.exists() {
Some((target, library))
} else {
None
}
})
.collect()
}

fn copy_into_jni_libs(
&self,
metadata: &CrateMetadata,
jni_libs: &Utf8Path,
target_files: &HashMap<Target, Utf8PathBuf>,
) -> Result<()> {
println!("-- Copying into jniLibs directory");
println!("rm -Rf {jni_libs}");
rm_dir(jni_libs)?;
for (target, library) in target_files {
let dst_dir = jni_libs.join(target.to_string());
mk_dir(&dst_dir)?;

let dst_lib = dst_dir.join(metadata.library_file(Some(target.triple())));
fs::copy(&src_lib, &dst_lib)?;

target_files.push(src_lib);
println!("cp {library} {dst_lib}");
fs::copy(library, &dst_lib)?;
}

Ok(target_files)
Ok(())
}

pub(crate) fn project_config(&self) -> Result<ProjectConfig> {
self.config.clone().try_into()
}

pub(crate) fn config(&self) -> Utf8PathBuf {
self.config.clone()
}
}

#[derive(Debug, Deserialize, Default, Clone)]
#[derive(Debug, Deserialize, Default, Clone, Hash, PartialEq, Eq)]
pub enum Target {
#[serde(rename = "armeabi-v7a")]
ArmeabiV7a,
Expand Down
50 changes: 14 additions & 36 deletions crates/ubrn_cli/src/building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ use anyhow::{anyhow, Result};
use camino::Utf8PathBuf;
use clap::{Args, Subcommand};
use serde::Deserialize;
use ubrn_bindgen::{BindingsArgs, OutputArgs, SourceArgs};
use ubrn_common::CrateMetadata;

use crate::{android::AndroidArgs, config::ProjectConfig, ios::IOsArgs};
use crate::{android::AndroidArgs, generate::GenerateAllArgs, ios::IOsArgs};

#[derive(Args, Debug)]
pub(crate) struct BuildArgs {
Expand All @@ -30,44 +29,15 @@ pub(crate) enum BuildCmd {
impl BuildArgs {
pub(crate) fn build(&self) -> Result<()> {
let lib_file = self.cmd.build()?;
if self.and_generate() {
if self.cmd.and_generate() {
self.generate(lib_file)?;
}

Ok(())
}

fn generate(&self, lib_file: Utf8PathBuf) -> Result<()> {
let project = self.cmd.project_config()?;
let root = project.project_root();
let pwd = ubrn_common::pwd()?;
let modules = {
let dir = project.crate_.directory()?;
ubrn_common::cd(&dir)?;
let ts_dir = project.bindings.ts_path(root);
let cpp_dir = project.bindings.cpp_path(root);
let config = project.bindings.uniffi_toml_path(root);
if let Some(ref file) = config {
if !file.exists() {
anyhow::bail!("uniffi.toml file {:?} does not exist. Either delete the uniffiToml property or supply a file", file)
}
}
let bindings = BindingsArgs::new(
SourceArgs::library(&lib_file).with_config(config),
OutputArgs::new(&ts_dir, &cpp_dir, false),
);

bindings.run()?
};
ubrn_common::cd(&pwd)?;

let rust_crate = project.crate_.metadata()?;
crate::codegen::render_files(project, rust_crate, modules)?;
Ok(())
}

fn and_generate(&self) -> bool {
self.cmd.and_generate()
GenerateAllArgs::new(lib_file, self.cmd.config()).run()
}
}

Expand All @@ -84,10 +54,10 @@ impl BuildCmd {
.ok_or_else(|| anyhow!("No targets were specified"))
}

pub(crate) fn project_config(&self) -> Result<ProjectConfig> {
fn config(&self) -> Utf8PathBuf {
match self {
Self::Android(a) => a.project_config(),
Self::Ios(a) => a.project_config(),
Self::Android(a) => a.config(),
Self::Ios(a) => a.config(),
}
}

Expand All @@ -109,6 +79,14 @@ pub(crate) struct CommonBuildArgs {
#[clap(long, short, default_value = "false")]
pub(crate) release: bool,

/// If the Rust library has been built for at least one target, then
/// don't re-run cargo build.
///
/// This may be useful if you are using a pre-built library or are
/// managing the build process yourself.
#[clap(long)]
pub(crate) no_cargo: bool,

/// Optionally generate the bindings and turbo-module code for the crate
#[clap(long = "and-generate", short = 'g')]
pub(crate) and_generate: bool,
Expand Down
63 changes: 61 additions & 2 deletions crates/ubrn_cli/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/
*/
use anyhow::Result;
use camino::Utf8PathBuf;
use clap::{Args, Subcommand};
use ubrn_bindgen::BindingsArgs;
use ubrn_bindgen::{BindingsArgs, OutputArgs, SourceArgs};

use crate::codegen::TurboModuleArgs;
use crate::{codegen::TurboModuleArgs, config::ProjectConfig};

#[derive(Args, Debug)]
pub(crate) struct GenerateArgs {
Expand All @@ -27,6 +28,11 @@ pub(crate) enum GenerateCmd {
Bindings(BindingsArgs),
/// Generate the TurboModule code to plug the bindings into the app
TurboModule(TurboModuleArgs),
/// Generate the Bindings and TurboModule code from a library
/// file and a YAML config file.
///
/// This is the second step of the `--and-generate` option of the build command.
All(GenerateAllArgs),
}

impl GenerateCmd {
Expand All @@ -40,6 +46,59 @@ impl GenerateCmd {
t.run()?;
Ok(())
}
Self::All(t) => {
t.run()?;
Ok(())
}
}
}
}

#[derive(Args, Debug)]
pub(crate) struct GenerateAllArgs {
/// The configuration file for this project
#[clap(long)]
config: Utf8PathBuf,

/// A path to staticlib file.
lib_file: Utf8PathBuf,
}

impl GenerateAllArgs {
pub(crate) fn new(lib_file: Utf8PathBuf, config: Utf8PathBuf) -> Self {
Self { lib_file, config }
}

pub(crate) fn run(&self) -> Result<()> {
let project = self.project_config()?;
let root = project.project_root();
let pwd = ubrn_common::pwd()?;
let lib_file = pwd.join(&self.lib_file);
let modules = {
let dir = project.crate_.directory()?;
ubrn_common::cd(&dir)?;
let ts_dir = project.bindings.ts_path(root);
let cpp_dir = project.bindings.cpp_path(root);
let config = project.bindings.uniffi_toml_path(root);
if let Some(ref file) = config {
if !file.exists() {
anyhow::bail!("uniffi.toml file {:?} does not exist. Either delete the uniffiToml property or supply a file", file)
}
}
let bindings = BindingsArgs::new(
SourceArgs::library(&lib_file).with_config(config),
OutputArgs::new(&ts_dir, &cpp_dir, false),
);

bindings.run()?
};
ubrn_common::cd(&pwd)?;
let rust_crate = project.crate_.metadata()?;
crate::codegen::render_files(project, rust_crate, modules)?;
Ok(())
}

fn project_config(&self) -> Result<ProjectConfig> {
self.config.clone().try_into()
}
}
Loading

0 comments on commit 90c9e92

Please sign in to comment.