Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
*.swp
my_package/
src/new/includes.rs
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ Tools for developing on Uqbar
Install with cargo:

```bash
# Get utility to build Python:
pip3 install componentize-py==0.7.1

# Get nvm, node, npm for building front-ends:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Then, in a new terminal:
nvm install node
nvm install-latest-npm

# Install `uqdev` tools:
cargo install --git https://github.com/uqbar-dao/uqdev
```

Expand Down
47 changes: 47 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::Path;

const NEW_DIR: &str = "src/new";
const TEMPLATES_DIR: &str = "src/new/templates";

fn main() -> io::Result<()> {
let output_path = Path::new(NEW_DIR).join("includes.rs");
let mut output_file = File::create(output_path)?;

writeln!(&mut output_file, "const PATH_TO_CONTENT: &[(&str, &str)] = &[")?;

visit_dirs(Path::new(TEMPLATES_DIR), &mut output_file)?;

writeln!(&mut output_file, "];")?;
Ok(())
}

fn visit_dirs(dir: &Path, output_file: &mut File) -> io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
visit_dirs(&path, output_file)?;
} else {
if path.extension().and_then(|s| s.to_str()) == Some("swp") {
continue;
}

let relative_path = path.strip_prefix(TEMPLATES_DIR).unwrap();
let path_str = relative_path.to_str().unwrap().replace("\\", "/");

let relative_path_from_includes = path.strip_prefix(NEW_DIR).unwrap();
let path_str_from_includes = relative_path_from_includes.to_str().unwrap().replace("\\", "/");
writeln!(
output_file,
" (\"{}\", include_str!(\"{}\")),",
path_str, path_str_from_includes
)?;
}
}
}
Ok(())
}

66 changes: 55 additions & 11 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::process::{Command, Stdio};

use reqwest;
Expand All @@ -16,6 +16,13 @@ struct CargoPackage {
name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Metadata {
package: String,
publisher: String,
version: [u32; 3],
}

pub fn run_command(cmd: &mut Command) -> io::Result<()> {
let status = cmd.status()?;
if status.success() {
Expand Down Expand Up @@ -77,22 +84,59 @@ pub async fn compile_package_and_ui(package_dir: &Path, verbose: bool) -> anyhow
}

pub async fn compile_package(package_dir: &Path, verbose: bool) -> anyhow::Result<()> {
// TODO: When expanding to other languages, will no longer be
// able to use Cargo.toml as indicator of a process dir
// Look for subdirectories containing `Cargo.toml`
let rust_src_path = "src/lib.rs";
let python_src_path = "src/lib.py";
for entry in package_dir.read_dir()? {
let entry = entry?;
let path = entry.path();
if path.is_dir() && path.join("Cargo.toml").exists() {
compile_wasm_project(&path, verbose).await?;
if path.is_dir() {
if path.join(&rust_src_path).exists() {
compile_rust_wasm_process(&path, verbose).await?;
} else if path.join(&python_src_path).exists() {
compile_python_wasm_process(&path, verbose).await?;
}
}
}

Ok(())
}

async fn compile_wasm_project(process_dir: &Path, verbose: bool) -> anyhow::Result<()> {
println!("Compiling Uqbar process in {:?}...", process_dir);
async fn compile_python_wasm_process(
process_dir: &Path,
verbose: bool,
) -> anyhow::Result<()> {
println!("Compiling Python Uqbar process in {:?}...", process_dir);
let wit_dir = process_dir.join("wit");
fs::create_dir_all(&wit_dir)?;
let uqbar_wit_url = "https://raw.githubusercontent.com/uqbar-dao/uqwit/master/uqbar.wit";
download_file(uqbar_wit_url, &wit_dir.join("uqbar.wit")).await?;

let wasm_file_name = process_dir
.file_name()
.and_then(|s| s.to_str())
.unwrap();

run_command(Command::new("componentize-py")
.args(&[
"-d", "../wit/",
"-w", "process",
"componentize", "lib",
"-o", &format!("../../pkg/{wasm_file_name}.wasm")
])
.current_dir(process_dir.join("src"))
.stdout(if verbose { Stdio::inherit() } else { Stdio::null() })
.stderr(if verbose { Stdio::inherit() } else { Stdio::null() })
)?;

println!("Done compiling Python Uqbar process in {:?}.", process_dir);
Ok(())
}

async fn compile_rust_wasm_process(
process_dir: &Path,
verbose: bool,
) -> anyhow::Result<()> {
println!("Compiling Rust Uqbar process in {:?}...", process_dir);

// Paths
let bindings_dir = process_dir
Expand All @@ -107,8 +151,7 @@ async fn compile_wasm_project(process_dir: &Path, verbose: bool) -> anyhow::Resu
// Check and download uqbar.wit if wit_dir does not exist
//if !wit_dir.exists() { // TODO: do a smarter check; this check will fail when remote has updated v
fs::create_dir_all(&wit_dir)?;
// let uqbar_wit_url = "https://raw.githubusercontent.com/uqbar-dao/uqwit/master/uqbar.wit";
let uqbar_wit_url = "https://raw.githubusercontent.com/uqbar-dao/uqwit/2bb0a6b3b860545871cd53f607ef2b4e1da7a451/uqbar.wit";
let uqbar_wit_url = "https://raw.githubusercontent.com/uqbar-dao/uqwit/master/uqbar.wit";
download_file(uqbar_wit_url, &wit_dir.join("uqbar.wit")).await?;

// Check and download wasi_snapshot_preview1.wasm if it does not exist
Expand Down Expand Up @@ -163,6 +206,7 @@ async fn compile_wasm_project(process_dir: &Path, verbose: bool) -> anyhow::Resu
let cargo_parsed = toml::from_str::<CargoFile>(&cargo_contents)?;
cargo_parsed.package.name
};

let wasm_file_prefix = Path::new("target/wasm32-wasi/release");
let wasm_file = wasm_file_prefix
.clone()
Expand Down Expand Up @@ -202,6 +246,6 @@ async fn compile_wasm_project(process_dir: &Path, verbose: bool) -> anyhow::Resu
.stderr(if verbose { Stdio::inherit() } else { Stdio::null() })
)?;

println!("Done compiling WASM project in {:?}.", process_dir);
println!("Done compiling Rust Uqbar process in {:?}.", process_dir);
Ok(())
}
26 changes: 25 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,16 @@ async fn execute(
None => new_dir_clone.file_name().unwrap().to_str().unwrap(),
};
let publisher = new_matches.get_one::<String>("PUBLISHER").unwrap();
let language: new::Language = new_matches.get_one::<String>("LANGUAGE").unwrap().into();
let template: new::Template = new_matches.get_one::<String>("TEMPLATE").unwrap().into();

new::execute(new_dir, package_name.to_string(), publisher.clone())
new::execute(
new_dir,
package_name.to_string(),
publisher.clone(),
language.clone(),
template.clone(),
)
},
Some(("run-tests", run_tests_matches)) => {
let config_path = match run_tests_matches.get_one::<String>("PATH") {
Expand Down Expand Up @@ -273,6 +281,22 @@ async fn main() -> anyhow::Result<()> {
.help("Name of the publisher")
.default_value("template.uq")
)
.arg(Arg::new("LANGUAGE")
.action(ArgAction::Set)
.short('l')
.long("language")
.help("Programming language of the template")
.value_parser(["rust", "python"])
.default_value("rust")
)
.arg(Arg::new("TEMPLATE")
.action(ArgAction::Set)
.short('t')
.long("template")
.help("Template to create")
.value_parser(["chat"])
.default_value("chat")
)
)
.subcommand(Command::new("run-tests")
.about("Run Uqbar tests")
Expand Down
125 changes: 76 additions & 49 deletions src/new/mod.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
use std::{fs, path::{PathBuf, Path}, collections::HashMap};

const PATH_TO_CONTENT: &[(&str, &str)] = &[
(".gitignore", include_str!("templates/rust/chat/.gitignore")),
("{package_name}/Cargo.toml", include_str!("templates/rust/chat/chat/Cargo.toml")),
("{package_name}/src/lib.rs", include_str!("templates/rust/chat/chat/src/lib.rs")),
("pkg/manifest.json", include_str!("templates/rust/chat/pkg/manifest.json")),
("pkg/metadata.json", include_str!("templates/rust/chat/pkg/metadata.json")),
("ui/.eslintrc.cjs", include_str!("templates/rust/chat/ui/.eslintrc.cjs")),
("ui/.gitignore", include_str!("templates/rust/chat/ui/.gitignore")),
("ui/README.md", include_str!("templates/rust/chat/ui/README.md")),
("ui/package.json", include_str!("templates/rust/chat/ui/package.json")),
("ui/package-lock.json", include_str!("templates/rust/chat/ui/package-lock.json")),
("ui/tsconfig.json", include_str!("templates/rust/chat/ui/tsconfig.json")),
("ui/tsconfig.node.json", include_str!("templates/rust/chat/ui/tsconfig.node.json")),
("ui/vite.config.ts", include_str!("templates/rust/chat/ui/vite.config.ts")),
("ui/index.html", include_str!("templates/rust/chat/ui/index.html")),
("ui/public/assets/vite.svg", include_str!("templates/rust/chat/ui/public/assets/vite.svg")),
("ui/src/App.css", include_str!("templates/rust/chat/ui/src/App.css")),
("ui/src/App.tsx", include_str!("templates/rust/chat/ui/src/App.tsx")),
("ui/src/assets/react.svg", include_str!("templates/rust/chat/ui/src/assets/react.svg")),
("ui/src/index.css", include_str!("templates/rust/chat/ui/src/index.css")),
("ui/src/main.tsx", include_str!("templates/rust/chat/ui/src/main.tsx")),
("ui/src/store/chat.ts", include_str!("templates/rust/chat/ui/src/store/chat.ts")),
("ui/src/types/Chat.ts", include_str!("templates/rust/chat/ui/src/types/Chat.ts")),
("ui/src/types/global.ts", include_str!("templates/rust/chat/ui/src/types/global.ts")),
("ui/src/vite-env.d.ts", include_str!("templates/rust/chat/ui/src/vite-env.d.ts")),
];
include!("includes.rs");

pub fn execute(new_dir: PathBuf, package_name: String, publisher: String) -> anyhow::Result<()> {
#[derive(Clone)]
pub enum Language {
Rust,
Python,
}

#[derive(Clone)]
pub enum Template {
Chat,
}

impl Language {
fn to_string(&self) -> String {
match self {
Language::Rust => "rust",
Language::Python => "python",
}.to_string()
}
}

impl Template {
fn to_string(&self) -> String {
match self {
Template::Chat => "chat",
}.to_string()
}
}

impl From<&String> for Language {
fn from(s: &String) -> Self {
match s.as_str() {
"rust" => Language::Rust,
"python" => Language::Python,
_ => panic!("uqdev: language must be 'rust' or 'python'; not '{s}'"),
}
}
}

impl From<&String> for Template {
fn from(s: &String) -> Self {
match s.as_str() {
"chat" => Template::Chat,
_ => panic!("uqdev: template must be 'chat'; not '{s}'"),
}
}
}

pub fn execute(
new_dir: PathBuf,
package_name: String,
publisher: String,
language: Language,
template: Template,
) -> anyhow::Result<()> {
// Check if the directory already exists
if new_dir.exists() {
let error = format!(
Expand All @@ -38,35 +66,34 @@ pub fn execute(new_dir: PathBuf, package_name: String, publisher: String) -> any
return Err(anyhow::anyhow!(error));
}

let mut path_to_content: HashMap<String, String> = PATH_TO_CONTENT
let template_prefix = format!("{}/{}/", language.to_string(), template.to_string());
let path_to_content: HashMap<String, String> = PATH_TO_CONTENT
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.filter_map(|(k, v)| {
k
.strip_prefix(&template_prefix)
.and_then(|stripped| {
let key = stripped
.replace("{package_name}", &package_name)
.to_string();
let val = v
.replace("{package_name}", &package_name)
.replace("{publisher}", &publisher)
.to_string();
Some((key, val))
})
})
.collect();
for entry in &[
".gitignore",
"{package_name}/Cargo.toml",
"{package_name}/src/lib.rs",
"pkg/manifest.json",
"pkg/metadata.json",
] {
path_to_content
.entry(entry.to_string())
.and_modify(|c| *c = c.replace("{package_name}", &package_name))
.and_modify(|c| *c = c.replace("{publisher}", &publisher));
}

// Create the template directory and subdirectories
fs::create_dir_all(new_dir.join("pkg"))?;
fs::create_dir_all(new_dir.join(&package_name).join("src"))?;
fs::create_dir_all(new_dir.join("ui/public/assets"))?;
fs::create_dir_all(new_dir.join("ui/src/assets"))?;
fs::create_dir_all(new_dir.join("ui/src/store"))?;
fs::create_dir_all(new_dir.join("ui/src/types"))?;
path_to_content
.keys()
.filter_map(|p| Path::new(p).parent())
.try_for_each(|p| fs::create_dir_all(new_dir.join(p)))?;

// Copy the template files
for (path, content) in path_to_content {
let path = path.replace("{package_name}", &package_name);
fs::write(new_dir.join(path.replace("{package_name}", &package_name)), content)?;
fs::write(new_dir.join(path), content)?;
}

println!("Template directory created successfully at {:?}.", new_dir);
Expand Down
6 changes: 6 additions & 0 deletions src/new/templates/python/chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*/target/
pkg/*.wasm
*.swp
*.swo
*/wasi_snapshot_preview1.wasm
*/wit/
13 changes: 13 additions & 0 deletions src/new/templates/python/chat/pkg/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[
{
"process_name": "{package_name}",
"process_wasm_path": "/{package_name}.wasm",
"on_exit": "Restart",
"request_networking": true,
"request_messaging": [
"net:sys:uqbar"
],
"grant_messaging": [],
"public": true
}
]
5 changes: 5 additions & 0 deletions src/new/templates/python/chat/pkg/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"package": "{package_name}",
"publisher": "{publisher}",
"version": [0, 1, 0]
}
Loading