Skip to content

Commit

Permalink
fetch possible example contracts at compile time (#1244)
Browse files Browse the repository at this point in the history
* Fetch possible example contracts at compile time
* Cache example contracts in target dir

Co-authored-by: Willem Wyndham <[email protected]>
  • Loading branch information
elizabethengelman and willemneal authored Mar 13, 2024
1 parent db203d3 commit 02fa2f0
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 51 deletions.
4 changes: 4 additions & 0 deletions cmd/soroban-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ openssl = { version = "=0.10.55", features = ["vendored"] }

[build-dependencies]
crate-git-revision = "0.0.4"
serde.workspace = true
thiserror.workspace = true
ureq = { version = "2.9.1", features = ["json"] }


[dev-dependencies]
assert_cmd = "2.0.4"
Expand Down
121 changes: 121 additions & 0 deletions cmd/soroban-cli/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,124 @@
fn main() {
crate_git_revision::init();
build_helper::set_example_contracts();
}

mod build_helper {
use std::{
fs::{metadata, File, Metadata},
io::{self, Write},
path::{Path, PathBuf},
};

const GITHUB_API_URL: &str =
"https://api.github.com/repos/stellar/soroban-examples/git/trees/main?recursive=1";

pub fn set_example_contracts() {
let example_contracts = get_example_contracts().unwrap();
let w = &mut std::io::stdout();
set_example_contracts_env_var(w, &example_contracts).unwrap();
}

#[derive(serde::Deserialize, Debug)]
struct RepoPath {
path: String,
#[serde(rename = "type")]
type_field: String,
}

#[derive(serde::Deserialize, Debug)]
struct ReqBody {
tree: Vec<RepoPath>,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to complete get request")]
UreqError(#[from] Box<ureq::Error>),

#[error("Io error: {0}")]
IoError(#[from] std::io::Error),
}

fn get_example_contracts() -> Result<String, Error> {
if file_exists(&cached_example_contracts_file_path()) {
let example_contracts = std::fs::read_to_string(cached_example_contracts_file_path())?;
return Ok(example_contracts);
}

Ok(fetch_and_cache_example_contracts())
}

fn fetch_and_cache_example_contracts() -> String {
let example_contracts = fetch_example_contracts().unwrap().join(",");
let cached_example_contracts = target_dir().join("example_contracts.txt");

if let Err(err) = write_cache(&cached_example_contracts, &example_contracts) {
eprintln!("Error writing cache: {err}");
}

example_contracts
}

fn fetch_example_contracts() -> Result<Vec<String>, Error> {
let body: ReqBody = ureq::get(GITHUB_API_URL)
.call()
.map_err(Box::new)?
.into_json()?;
let mut valid_examples = Vec::new();
for item in body.tree {
if item.type_field == "blob"
|| item.path.starts_with('.')
|| item.path.contains('/')
|| item.path == "hello_world"
{
continue;
}

valid_examples.push(item.path);
}

Ok(valid_examples)
}

fn set_example_contracts_env_var(
w: &mut impl std::io::Write,
example_contracts: &str,
) -> std::io::Result<()> {
writeln!(w, "cargo:rustc-env=EXAMPLE_CONTRACTS={example_contracts}")?;
Ok(())
}

fn cached_example_contracts_file_path() -> PathBuf {
target_dir().join("example_contracts.txt")
}

fn target_dir() -> PathBuf {
project_root().join("target")
}

fn project_root() -> PathBuf {
Path::new(&env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.unwrap()
.to_path_buf()
}

fn write_cache(cache_file_path: &Path, data: &str) -> io::Result<()> {
// Create or open the cache file
let mut file = File::create(cache_file_path)?;

// Write the data to the cache file
file.write_all(data.as_bytes())?;

Ok(())
}

fn file_exists(file_path: &Path) -> bool {
metadata(file_path)
.as_ref()
.map(Metadata::is_file)
.unwrap_or(false)
}
}
68 changes: 17 additions & 51 deletions cmd/soroban-cli/src/commands/contract/init.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::{
env,
ffi::OsStr,
fs::{copy, create_dir_all, metadata, read_dir, read_to_string, write, File, OpenOptions},
fs::{
copy, create_dir_all, metadata, read_dir, read_to_string, write, File, Metadata,
OpenOptions,
},
io::{self, Read, Write},
num::NonZeroU32,
path::Path,
Expand All @@ -17,10 +20,12 @@ use gix::{clone, create, open, progress, remote};
use rust_embed::RustEmbed;
use serde_json::{from_str, json, to_string_pretty, Error as JsonError, Value as JsonValue};
use toml_edit::{Document, Formatted, InlineTable, Item, TomlError, Value as TomlValue};
use ureq::{get, Error as UreqError};
use ureq::get;

const SOROBAN_EXAMPLES_URL: &str = "https://github.com/stellar/soroban-examples.git";
const GITHUB_URL: &str = "https://github.com";
const WITH_EXAMPLE_LONG_HELP_TEXT: &str =
"An optional flag to specify Soroban example contracts to include. A hello-world contract will be included by default.";

#[derive(Clone, Debug, ValueEnum, PartialEq)]
pub enum FrontendTemplate {
Expand All @@ -33,7 +38,7 @@ pub enum FrontendTemplate {
pub struct Cmd {
pub project_path: String,

#[arg(short, long, num_args = 1.., value_parser=possible_example_values(), long_help=with_example_help())]
#[arg(short, long, num_args = 1.., value_parser=possible_example_values(), long_help=WITH_EXAMPLE_LONG_HELP_TEXT)]
pub with_example: Vec<String>,

#[arg(
Expand All @@ -46,46 +51,11 @@ pub struct Cmd {
}

fn possible_example_values() -> ValueParser {
let parser = PossibleValuesParser::new(
[
"account",
"alloc",
"atomic_multiswap",
"atomic_swap",
"auth",
"cross_contract",
"custom_types",
"deep_contract_auth",
"deployer",
"errors",
"eth_abi",
"events",
"fuzzing",
"increment",
"liquidity_pool",
"logging",
"mint-lock",
"simple_account",
"single_offer",
"timelock",
"token",
"upgradeable_contract",
"workspace",
]
.iter()
.map(PossibleValue::new),
);
let example_contracts = env!("EXAMPLE_CONTRACTS").split(',').collect::<Vec<&str>>();
let parser = PossibleValuesParser::new(example_contracts.iter().map(PossibleValue::new));
parser.into()
}

fn with_example_help() -> String {
if check_internet_connection() {
"An optional flag to specify Soroban example contracts to include. A hello-world contract will be included by default.".to_owned()
} else {
"⚠️ Failed to fetch additional example contracts from soroban-examples repo. You can continue with initializing - the default hello_world contract will still be included".to_owned()
}
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Io error: {0}")]
Expand All @@ -105,9 +75,6 @@ pub enum Error {
#[error("Failed to parse toml file: {0}")]
TomlParseError(#[from] TomlError),

#[error("Failed to complete get request")]
UreqError(#[from] Box<UreqError>),

#[error("Failed to parse package.json file: {0}")]
JsonParseError(#[from] JsonError),

Expand Down Expand Up @@ -184,7 +151,7 @@ fn copy_template_files(project_path: &Path) -> Result<(), Error> {
for item in TemplateFiles::iter() {
let mut to = project_path.join(item.as_ref());

if file_exists(&to.to_string_lossy()) {
if file_exists(&to) {
println!(
"ℹ️ Skipped creating {} as it already exists",
&to.to_string_lossy()
Expand Down Expand Up @@ -250,7 +217,7 @@ fn copy_contents(from: &Path, to: &Path) -> Result<(), Error> {
})?;
copy_contents(&path, &new_path)?;
} else {
if file_exists(&new_path.to_string_lossy()) {
if file_exists(&new_path) {
if new_path.to_string_lossy().contains(".gitignore") {
append_contents(&path, &new_path)?;
}
Expand Down Expand Up @@ -280,12 +247,11 @@ fn copy_contents(from: &Path, to: &Path) -> Result<(), Error> {
Ok(())
}

fn file_exists(file_path: &str) -> bool {
if let Ok(metadata) = metadata(file_path) {
metadata.is_file()
} else {
false
}
fn file_exists(file_path: &Path) -> bool {
metadata(file_path)
.as_ref()
.map(Metadata::is_file)
.unwrap_or(false)
}

fn include_example_contracts(contracts: &[String]) -> bool {
Expand Down

0 comments on commit 02fa2f0

Please sign in to comment.