Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: return JSON-based ABI generation #73

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions cargo-near/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ clap = { version = "3.2", features = ["derive", "env"] }
colored = "2.0"
env_logger = "0.9"
log = "0.4"
prettyplease = "0.1"
toml = "0.5"
serde_json = "1.0"
symbolic-debuginfo = "8.8"
schemars = "0.8"
syn = "1.0"
quote = "1.0"
near-abi = { version = "0.1.0-pre.0", features = ["__chunked-entries"] }
libloading = "0.7.3"
zstd = "0.11"
165 changes: 165 additions & 0 deletions cargo-near/src/abi/generation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use crate::cargo::manifest::CargoManifestPath;
use crate::cargo::metadata::CrateMetadata;
use anyhow::Context;
use quote::{format_ident, quote};
use std::path::{Path, PathBuf};
use std::{collections::HashSet, fs};
use toml::value;

pub(crate) fn generate_toml(
manifest_path: &CargoManifestPath,
crate_metadata: &CrateMetadata,
) -> anyhow::Result<String> {
let original_cargo_toml = fs::read_to_string(&manifest_path.path)?;
let original_cargo_toml: toml::value::Table = toml::from_str(&original_cargo_toml)?;

let mut near_sdk = original_cargo_toml
.get("dependencies")
.context("Cargo.toml [dependencies] section not found")?
.get("near-sdk")
.context("`near-sdk` dependency not found")?
.as_table()
.context("`near-sdk` dependency should be a table")?
.clone();

let cargo_toml = include_str!("../../templates/_Cargo.toml");
let mut cargo_toml: toml::value::Table = toml::from_str(cargo_toml)?;
let package = cargo_toml
.get_mut("package")
.context("Cargo.toml template [package] section not found")?
.as_table_mut()
.context("expected Cargo.toml template [package] section to be a table")?;
package.insert(
"name".to_string(),
toml::value::Value::String(format!("{}-near-abi-gen", crate_metadata.root_package.name)),
);
let deps = cargo_toml
.get_mut("dependencies")
.context("Cargo.toml template [dependencies] section not found")?
.as_table_mut()
.context("expected Cargo.toml template [dependencies] section to be a table")?;

// Make near-sdk dependency not use default features to save on compilation time, but ensure `abi` is enabled
near_sdk.remove("optional");
near_sdk.insert("default-features".to_string(), value::Value::Boolean(false));
near_sdk.insert(
"features".to_string(),
value::Value::Array(vec![value::Value::String("abi".to_string())]),
);

// If near-sdk is a local path dependency, then convert the path to be absolute
if let Some(near_sdk_path) = near_sdk.get_mut("path") {
let path = near_sdk_path
.as_str()
.context("`near-sdk` path should be a string")?;
let path = manifest_path.directory()?.join(PathBuf::from(path));
*near_sdk_path = value::Value::String(path.canonicalize()?.to_string_lossy().into());
}

deps.insert("near-sdk".into(), near_sdk.into());

let cargo_toml = toml::to_string(&cargo_toml)?;

log::debug!("Cargo.toml contents:\n{}", &cargo_toml);

Ok(cargo_toml)
}

pub(crate) fn generate_build_rs(dylib_path: &Path) -> anyhow::Result<String> {
let dylib_dir = dylib_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"Unable to infer the directory containing dylib file: {}",
dylib_path.display()
)
})?;
let dylib_name = dylib_path
.file_stem()
.ok_or_else(|| anyhow::anyhow!("Generated dylib is not a file: {}", dylib_path.display()))?
.to_str()
.ok_or_else(|| {
anyhow::anyhow!(
"Unable to infer the directory containing dylib file: {}",
dylib_path.display()
)
})?;

let dylib_name = if let Some(dylib_name_stripped) = dylib_name.strip_prefix("lib") {
dylib_name_stripped
} else {
anyhow::bail!(
"Expected the generated dylib file to start with 'lib', but got '{}'",
dylib_name
);
};

let cargo_link_lib = format!("cargo:rustc-link-lib=dylib={}", &dylib_name);
let cargo_link_search = format!("cargo:rustc-link-search=all={}", dylib_dir.display());

let build_rs = quote! {
fn main() {
println!(#cargo_link_lib);
println!(#cargo_link_search);
}
}
.to_string();
let build_rs_file = syn::parse_file(&build_rs).unwrap();
let build_rs = prettyplease::unparse(&build_rs_file);

log::debug!("build.rs contents:\n{}", &build_rs);

Ok(build_rs)
}

pub(crate) fn generate_main_rs(dylib_path: &Path) -> anyhow::Result<String> {
let dylib_file_contents = fs::read(&dylib_path)?;
let object = symbolic_debuginfo::Object::parse(&dylib_file_contents)?;
log::debug!(
"A dylib was built at {:?} with format {} for architecture {}",
&dylib_path,
&object.file_format(),
&object.arch()
);
let near_abi_symbols = object
.symbols()
.flat_map(|sym| sym.name)
.filter(|sym_name| sym_name.starts_with("__near_abi_"))
.map(|sym_name| sym_name.to_string())
.collect::<HashSet<_>>();
if near_abi_symbols.is_empty() {
anyhow::bail!("No NEAR ABI symbols found in the dylib");
}
log::debug!("Detected NEAR ABI symbols: {:?}", &near_abi_symbols);

let near_abi_function_defs = near_abi_symbols.iter().map(|s| {
let name = format_ident!("{}", s);
quote! {
fn #name() -> near_sdk::__private::ChunkedAbiEntry;
}
});
let near_abi_function_invocations = near_abi_symbols.iter().map(|s| {
let name = format_ident!("{}", s);
quote! {
unsafe { #name() }
}
});

let main_rs = quote! {
extern "Rust" {
#(#near_abi_function_defs)*
}

fn main() -> Result<(), std::io::Error> {
let abi_entries: Vec<near_sdk::__private::ChunkedAbiEntry> = vec![#(#near_abi_function_invocations),*];
let contents = serde_json::to_string_pretty(&abi_entries)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can actually move this bit to the SDK, simplifying things. See alt: #74

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, it may look similar but there is an important difference that I have touched upon here: #74 (review)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking a Rust String might be safe, but since we don't have that guarantee, it's just best to switch to the C ABI.

print!("{}", contents);
Ok(())
}
}
.to_string();
let main_rs_file = syn::parse_file(&main_rs).unwrap();
let main_rs = prettyplease::unparse(&main_rs_file);

log::debug!("main.rs contents:\n{}", &main_rs);

Ok(main_rs)
}
74 changes: 67 additions & 7 deletions cargo-near/src/abi.rs → cargo-near/src/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ use colored::Colorize;
use near_abi::AbiRoot;
use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader, Read};
use std::path::PathBuf;
use std::process::{ChildStderr, ChildStdout};

mod generation;

/// ABI generation result.
pub(crate) struct AbiResult {
Expand All @@ -29,6 +33,15 @@ pub(crate) fn generate_abi(
generate_docs: bool,
hide_warnings: bool,
) -> anyhow::Result<AbiRoot> {
let near_abi_gen_dir = &crate_metadata
.target_directory
.join(crate_metadata.root_package.name.clone() + "-near-abi-gen");
fs::create_dir_all(near_abi_gen_dir)?;
log::debug!(
"Using temp Cargo workspace at '{}'",
near_abi_gen_dir.display()
);

let root_node = crate_metadata
.raw_metadata
.resolve
Expand Down Expand Up @@ -86,18 +99,65 @@ pub(crate) fn generate_abi(
hide_warnings,
)?;

let mut contract_abi = util::handle_step("Extracting ABI...", || {
let abi_entries = util::extract_abi_entries(&dylib_artifact.path)?;
anyhow::Ok(
near_abi::__private::ChunkedAbiEntry::combine(abi_entries)?
.into_abi_root(extract_metadata(crate_metadata)),
)
})?;
util::print_step("Extracting ABI");
if dylib_artifact.fresh {
let cargo_toml = generation::generate_toml(&crate_metadata.manifest_path, crate_metadata)?;
fs::write(near_abi_gen_dir.join("Cargo.toml"), cargo_toml)?;

let build_rs = generation::generate_build_rs(&dylib_artifact.path)?;
fs::write(near_abi_gen_dir.join("build.rs"), build_rs)?;

let main_rs = generation::generate_main_rs(&dylib_artifact.path)?;
fs::write(near_abi_gen_dir.join("main.rs"), main_rs)?;
}

let (stdout, _) = util::invoke_cargo_generic(
"run",
[
"--package",
&format!("{}-near-abi-gen", crate_metadata.root_package.name),
]
.iter(),
Some(near_abi_gen_dir),
vec![(
"LD_LIBRARY_PATH",
dylib_artifact
.path
.parent()
.unwrap()
.to_string_lossy()
.to_string(),
)],
stdout_fn,
stderr_fn,
)?;

// This will fail to deserialize if there is a `schema_version` mismatch
let mut contract_abi = near_abi::__private::ChunkedAbiEntry::combine(
serde_json::from_slice::<Vec<_>>(&stdout)?.into_iter(),
)?
.into_abi_root(extract_metadata(crate_metadata));

if !generate_docs {
strip_docs(&mut contract_abi);
}

fn stdout_fn(mut child_stdout: ChildStdout) -> Result<Vec<u8>, std::io::Error> {
let mut buf = Vec::new();
child_stdout.read_to_end(&mut buf)?;
Ok(buf)
}

fn stderr_fn(child_stderr: ChildStderr) -> Result<(), std::io::Error> {
let stderr_reader = BufReader::new(child_stderr);
let stderr_lines = stderr_reader.lines();
for line in stderr_lines {
eprintln!(" │ {}", line.expect("failed to read cargo stderr"));
}

Ok(())
}

Ok(contract_abi)
}

Expand Down
Loading