29 Commits

Author SHA1 Message Date
10956e7af4 feat: with mold
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:44:26 +01:00
4f72b4fdae feat: with htmx
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 11:58:22 +01:00
ec029c81db chore(deps): update rust crate eyre to 0.6.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-17 17:40:10 +00:00
ddde6c0734 fix(deps): update rust crate async-scoped to 0.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-16 05:30:17 +00:00
cc1c356ad0 chore(deps): update rust crate tokio to 1.34.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-09 20:12:11 +00:00
e18d247e11 fix(deps): update rust crate futures to 0.3.29
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 15:02:32 +00:00
11323c0752 feat: add leptos
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-22 12:09:57 +02:00
39d15b5d7f chore(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-21 11:39:12 +00:00
bdaea19ac6 feat: ignore cache
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 13:36:51 +02:00
2d57b4f3b4 feat: update lock
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 13:15:24 +02:00
52914e08e6 feat: with updated dagger-sdk
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-10-21 12:45:33 +02:00
e2c7f46378 fix(git): make sure we actually fail when running an invalid git command
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:47:29 +02:00
82289f2552 chore: with version 0.2.0
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:37:22 +02:00
2482987daf chore: publish
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:36:50 +02:00
614a3bc305 feat(rust-publish): with rust publish
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:29:24 +02:00
5e604d7a10 chore: add noop release script
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:22:47 +02:00
cuddle-please
a94fd3e68e chore(release): 0.2.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2023-08-12 19:14:42 +00:00
e2f1d79031 fix(ci): only set local url instead of insteadOf
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:12:45 +02:00
07c593bb08 fix(ci): trim remote_url newlines
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 21:00:58 +02:00
61c34b9fb8 fix(ci): trim remote_url newlines
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 20:57:21 +02:00
03784be431 fix(ci): repo should be ssh
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 18:54:06 +00:00
1cf349c3c6 fix(ci): make sure to run ssh as user git
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 20:26:13 +02:00
7277e06c0b chore: set fixed versions
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 20:14:27 +02:00
776db7274a fix: ci
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 18:08:04 +00:00
e1428a8fbb feat: with rust build and test
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 18:08:04 +00:00
a17e527b91 chore(deps): update rust crate tokio to 1.31.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-08-12 16:53:55 +00:00
5c53589c27 refactor(ci): move cuddle please image to cuddle
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-12 12:26:12 +02:00
8c3b5e660f feat(ci): with internal please action
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-11 18:23:05 +00:00
37054fa012 Add renovate.json
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-11 18:22:57 +00:00
66 changed files with 6134 additions and 631 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target /target
.env .env
.cuddle/ .cuddle/
target/

View File

@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.2.0] - 2023-08-12
### Added
- with rust build and test
- *(ci)* with internal please action
### Fixed
- *(ci)* only set local url instead of insteadOf
- *(ci)* trim remote_url newlines
- *(ci)* trim remote_url newlines
- *(ci)* repo should be ssh
- *(ci)* make sure to run ssh as user git
- ci
### Other
- set fixed versions
- *(deps)* update rust crate tokio to 1.31.0
- *(ci)* move cuddle please image to cuddle
- Add renovate.json
## [0.1.0] - 2023-08-11 ## [0.1.0] - 2023-08-11
### Added ### Added

609
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,25 @@
[workspace] [workspace]
members = [ members = ["crates/*", "examples/*", "ci"]
"crates/*",
"examples/*",
"ci"
]
resolver = "2" resolver = "2"
[workspace.dependencies] [workspace.package]
cuddle-components = {path = "crates/cuddle-components"} version = "0.2.0"
dagger-components = {path = "crates/dagger-components"} edition = "2021"
dagger-cuddle-please = {path = "crates/dagger-cuddle-please"} license = "MIT"
ci = {path = "ci"} authors = ["kjuulh <contact@kjuulh.io>"]
readme = "README.md"
repository = "https://git.front.kjuulh.io/kjuulh/dagger-components"
dagger-sdk = "0.2.22" [workspace.dependencies]
eyre = "0.6.8" cuddle-components = { path = "crates/cuddle-components" }
tokio = "1.30.0" dagger-components = { path = "crates/dagger-components" }
dotenv = "*" dagger-cuddle-please = { path = "crates/dagger-cuddle-please" }
dagger-rust = { path = "crates/dagger-rust" }
ci = { path = "ci" }
#dagger-sdk = "0.3.2"
dagger-sdk = {git = "https://github.com/kjuulh/dagger.git", branch = "feat/with-send-sync"}
eyre = "0.6.9"
tokio = "1.34.0"
dotenv = "0.15.0"
async-trait = "0.1.74"

View File

@@ -6,11 +6,14 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
dagger-sdk = "*" dagger-cuddle-please.workspace = true
dagger-rust.workspace = true
dagger-sdk.workspace = true
eyre = "*" eyre = "*"
color-eyre = "*" color-eyre = "*"
tokio = "1" tokio = "1"
clap = {version = "4", features = ["derive"]} clap = {version = "4", features = ["derive"]}
futures = "0.3.28" futures = "0.3.29"
async-scoped = { version = "0.7.1", features = ["tokio", "use-tokio"] } async-scoped = { version = "0.8.0", features = ["tokio", "use-tokio"] }
dotenv.workspace = true dotenv.workspace = true

View File

@@ -1,4 +1,3 @@
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@@ -51,6 +50,9 @@ pub struct GlobalArgs {
#[arg(long, global = true, help_heading = "Global")] #[arg(long, global = true, help_heading = "Global")]
rust_builder_image: Option<String>, rust_builder_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
cuddle_please_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")] #[arg(long, global = true, help_heading = "Global")]
source: Option<PathBuf>, source: Option<PathBuf>,
} }
@@ -67,39 +69,27 @@ async fn main() -> eyre::Result<()> {
match &cli.commands { match &cli.commands {
Commands::Local { command } => match command { Commands::Local { command } => match command {
LocalCommands::Test => { LocalCommands::Test => {
let base_image = test::execute(client, &cli.global).await?;
base_rust_image(client.clone(), &cli.global, &None, &"debug".into()).await?;
test::execute(client, &cli.global, base_image).await?;
} }
LocalCommands::PleaseRelease => todo!(), LocalCommands::PleaseRelease => todo!(),
}, },
Commands::PullRequest {} => { Commands::PullRequest {} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command) { async fn test(client: dagger_sdk::Query, cli: &Command) {
let args = &cli.global; let args = &cli.global;
let base_image = base_rust_image(client.clone(), args, &None, &"debug".into()) test::execute(client.clone(), args).await.unwrap();
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
} }
tokio::join!(test(client.clone(), &cli),); tokio::join!(test(client.clone(), &cli),);
} }
Commands::Main {} => { Commands::Main {} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command) { async fn test(client: dagger_sdk::Query, cli: &Command) {
let args = &cli.global; let args = &cli.global;
let base_image = base_rust_image(client.clone(), args, &None, &"debug".into()) test::execute(client.clone(), args).await.unwrap();
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
} }
async fn cuddle_please(client: Arc<dagger_sdk::Query>, cli: &Command) { async fn cuddle_please(client: dagger_sdk::Query, cli: &Command) {
run_release_please(client.clone(), &cli.global) run_release_please(client.clone(), &cli.global)
.await .await
.unwrap(); .unwrap();
@@ -110,7 +100,7 @@ async fn main() -> eyre::Result<()> {
cuddle_please(client.clone(), &cli) cuddle_please(client.clone(), &cli)
); );
} }
Commands::Release => todo!(), Commands::Release => {}
} }
Ok(()) Ok(())
@@ -119,262 +109,49 @@ async fn main() -> eyre::Result<()> {
mod please_release { mod please_release {
use std::sync::Arc; use std::sync::Arc;
use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePleaseAction};
use crate::GlobalArgs; use crate::GlobalArgs;
pub async fn run_release_please( pub async fn run_release_please(
client: Arc<dagger_sdk::Query>, client: dagger_sdk::Query,
args: &GlobalArgs, args: &GlobalArgs,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let build_image = client DaggerCuddlePleaseAction::dagger(client)
.container() .execute_src(&CuddlePleaseSrcArgs {
.from("kasperhermansen/cuddle-please:main-1691504183"); cuddle_image: args
.cuddle_please_image
let src = client .clone()
.git_opts( .unwrap_or("kasperhermansen/cuddle-please:latest".into()),
"https://git.front.kjuulh.io/kjuulh/dagger-components", server: dagger_cuddle_please::models::SrcServer::Gitea {
dagger_sdk::QueryGitOpts { token: std::env::var("CUDDLE_PLEASE_TOKEN")
experimental_service_host: None, .expect("CUDDLE_PLEASE_TOKEN to be present"),
keep_git_dir: Some(true),
}, },
) log_level: Some(dagger_cuddle_please::models::LogLevel::Debug),
.branch("main") })
.tree(); .await?;
let res = build_image
.with_secret_variable(
"CUDDLE_PLEASE_TOKEN",
client
.set_secret("CUDDLE_PLEASE_TOKEN", std::env::var("CUDDLE_PLEASE_TOKEN")?)
.id()
.await?,
)
.with_workdir("/mnt/app")
.with_directory(".", src.id().await?)
.with_exec(vec![
"git",
"remote",
"set-url",
"origin",
&format!(
"https://git:{}@git.front.kjuulh.io/kjuulh/dagger-components.git",
std::env::var("CUDDLE_PLEASE_TOKEN")?
),
])
.with_exec(vec![
"cuddle-please",
"release",
"--engine=gitea",
"--owner=kjuulh",
"--repo=dagger-components",
"--branch=main",
"--api-url=https://git.front.kjuulh.io",
"--log-level=debug",
]);
let exit_code = res.exit_code().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
let please_out = res.stdout().await?;
println!("{please_out}");
let please_out = res.stderr().await?;
println!("{please_out}");
Ok(()) Ok(())
} }
} }
mod test { mod test {
use std::sync::Arc; use std::{path::PathBuf, sync::Arc};
use dagger_rust::build::RustVersion;
use crate::GlobalArgs; use crate::GlobalArgs;
pub async fn execute( pub async fn execute(client: dagger_sdk::Query, _args: &GlobalArgs) -> eyre::Result<()> {
_client: Arc<dagger_sdk::Query>, dagger_rust::test::RustTest::new(client)
_args: &GlobalArgs, .test(
container: dagger_sdk::Container, None::<PathBuf>,
) -> eyre::Result<()> { RustVersion::Nightly,
let test_image = container &["crates/*", "examples/*", "ci"],
.pipeline("rust:test") &[],
.with_exec(vec!["apt", "update"]) )
.with_exec(vec!["apt", "install", "-y", "git"]) .await?;
.with_exec(vec!["cargo", "test"]);
let please_out = test_image.stdout().await?;
println!("{please_out}");
let please_out = test_image.stderr().await?;
println!("{please_out}");
test_image.exit_code().await?;
Ok(()) Ok(())
} }
} }
pub fn get_src(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<dagger_sdk::Directory> {
let directory = client.host().directory_opts(
args.source
.clone()
.unwrap_or(PathBuf::from("."))
.display()
.to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(vec!["node_modules/", ".git/", "target/"])
.build()?,
);
Ok(directory)
}
pub async fn get_rust_dep_src(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<dagger_sdk::Directory> {
let directory = client.host().directory_opts(
args.source
.clone()
.unwrap_or(PathBuf::from("."))
.display()
.to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.build()?,
);
Ok(directory)
}
pub async fn get_rust_skeleton_files(
client: Arc<dagger_sdk::Query>,
_args: &GlobalArgs,
) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
let mut rust_crates = vec![PathBuf::from("ci")];
let mut dirs = tokio::fs::read_dir("crates").await?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
rust_crates.push(entry.path())
}
}
let mut dirs = tokio::fs::read_dir("examples").await?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
rust_crates.push(entry.path())
}
}
fn create_skeleton_files(
directory: dagger_sdk::Directory,
path: &Path,
) -> eyre::Result<dagger_sdk::Directory> {
println!("found crates: {}", path.display());
let main_content = r#"
#[allow(dead_code)]
fn main() { panic!("should never be executed"); }"#;
let lib_content = r#"
#[allow(dead_code)]
fn some() { panic!("should never be executed"); }"#;
let directory = directory.with_new_file(
path.join("src").join("main.rs").display().to_string(),
main_content,
);
let directory = directory.with_new_file(
path.join("src").join("lib.rs").display().to_string(),
lib_content,
);
Ok(directory)
}
let mut directory = client.directory();
let mut crate_names = Vec::new();
for rust_crate in rust_crates.iter() {
if let Some(file_name) = rust_crate.file_name() {
crate_names.push(file_name.to_str().unwrap().to_string());
}
directory = create_skeleton_files(directory, rust_crate)?;
}
Ok((directory, crate_names))
}
pub async fn base_rust_image(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
platform: &Option<String>,
profile: &String,
) -> eyre::Result<dagger_sdk::Container> {
let dep_src = get_rust_dep_src(client.clone(), args).await?;
let (skeleton_files, crates) = get_rust_skeleton_files(client.clone(), args).await?;
let src = get_src(client.clone(), args)?;
let client = client.pipeline("rust_base_image");
let rust_target = match platform
.clone()
.unwrap_or("linux/amd64".to_string())
.as_str()
{
"linux/amd64" => "x86_64-unknown-linux-gnu",
"linux/arm64" => "aarch64-unknown-linux-gnu",
_ => eyre::bail!("architecture not supported"),
};
let rust_build_image = client
.container()
.from(
args.rust_builder_image
.as_ref()
.unwrap_or(&"rustlang/rust:nightly".into()),
)
.with_exec(vec!["rustup", "target", "add", rust_target])
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "jq"]);
let target_cache = client.cache_volume(format!("rust_target_{}", profile));
let mut build_options = vec!["cargo", "build", "--target", rust_target, "--workspace"];
if profile == "release" {
build_options.push("--release");
}
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src.id().await?)
.with_directory("/mnt/src/", skeleton_files.id().await?)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache.id().await?);
let exclude = crates
.iter()
.filter(|c| **c != "ci")
.map(|c| format!("**/*{}*", c.replace('-', "_")))
.collect::<Vec<_>>();
let exclude = exclude.iter().map(|c| c.as_str()).collect();
let incremental_dir = client.directory().with_directory_opts(
".",
rust_prebuild.directory("target").id().await?,
dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude),
include: None,
},
);
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo").id().await?,
)
.with_directory("target", incremental_dir.id().await?)
.with_directory("/mnt/src/", src.id().await?);
Ok(rust_with_src)
}

View File

@@ -1,11 +1,16 @@
[package] [package]
name = "dagger-cuddle-please" name = "dagger-cuddle-please"
version = "0.1.0" description = "A set of components for running cuddle-please in dagger"
edition = "2021" version.workspace = true
edition.workspace = true
authors.workspace = true
readme.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
dagger-sdk.workspace = true dagger-sdk.workspace = true
eyre.workspace = true eyre.workspace = true
async-trait = "*" async-trait.workspace = true

View File

@@ -1,4 +1,4 @@
use std::sync::Arc; use std::sync::{Arc, Mutex};
use models::{CuddlePleaseArgs, CuddlePleaseSrcArgs}; use models::{CuddlePleaseArgs, CuddlePleaseSrcArgs};
use traits::CuddlePlease; use traits::CuddlePlease;
@@ -80,11 +80,11 @@ pub mod traits {
} }
} }
pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease + Send + Sync + 'static>); pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease>);
impl DaggerCuddlePleaseAction { impl DaggerCuddlePleaseAction {
/// Create a [`traits::CuddlePlease`] client based on dagger /// Create a [`traits::CuddlePlease`] client based on dagger
pub fn dagger(client: Arc<dagger_sdk::Query>) -> Self { pub fn dagger(client: dagger_sdk::Query) -> Self {
Self(Arc::new(DaggerCuddlePlease::new(client))) Self(Arc::new(DaggerCuddlePlease::new(client)))
} }
@@ -104,31 +104,27 @@ impl DaggerCuddlePleaseAction {
#[derive(Clone)] #[derive(Clone)]
struct DaggerCuddlePlease { struct DaggerCuddlePlease {
client: Arc<dagger_sdk::Query>, client: dagger_sdk::Query,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl CuddlePlease for DaggerCuddlePlease { impl CuddlePlease for DaggerCuddlePlease {
async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> { async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
self.cuddle_please(self.client.clone(), args).await self.cuddle_please(args).await
} }
async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> { async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
self.cuddle_please_src(self.client.clone(), args).await self.cuddle_please_src(args).await
} }
} }
impl DaggerCuddlePlease { impl DaggerCuddlePlease {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self { pub fn new(client: dagger_sdk::Query) -> Self {
Self { client } Self { client }
} }
pub async fn cuddle_please( pub async fn cuddle_please(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
&self, let build_image = self.client.container().from(&args.cuddle_image);
client: Arc<dagger_sdk::Query>,
args: &CuddlePleaseArgs,
) -> eyre::Result<()> {
let build_image = client.container().from(&args.cuddle_image);
let repo_url = match &args.server { let repo_url = match &args.server {
Server::Gitea { Server::Gitea {
@@ -182,11 +178,12 @@ impl DaggerCuddlePlease {
}; };
let src = if args.use_ssh_socket { let src = if args.use_ssh_socket {
let socket = client let socket = self
.client
.host() .host()
.unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set")); .unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set"));
client self.client
.git_opts( .git_opts(
&repo_url, &repo_url,
dagger_sdk::QueryGitOpts { dagger_sdk::QueryGitOpts {
@@ -200,7 +197,7 @@ impl DaggerCuddlePlease {
ssh_known_hosts: None, ssh_known_hosts: None,
}) })
} else { } else {
client self.client
.git_opts( .git_opts(
&repo_url, &repo_url,
dagger_sdk::QueryGitOpts { dagger_sdk::QueryGitOpts {
@@ -215,19 +212,16 @@ impl DaggerCuddlePlease {
let res = build_image let res = build_image
.with_secret_variable( .with_secret_variable(
"CUDDLE_PLEASE_TOKEN", "CUDDLE_PLEASE_TOKEN",
client self.client.set_secret(
.set_secret( "CUDDLE_PLEASE_TOKEN",
"CUDDLE_PLEASE_TOKEN", match &args.server {
match &args.server { Server::Gitea { token, .. } => token,
Server::Gitea { token, .. } => token, Server::GitHub { token } => token,
Server::GitHub { token } => token, },
}, ),
)
.id()
.await?,
) )
.with_workdir("/mnt/app") .with_workdir("/mnt/app")
.with_directory(".", src.id().await?) .with_directory(".", src)
.with_exec(vec!["git", "remote", "set-url", "origin", &repo_url]) .with_exec(vec!["git", "remote", "set-url", "origin", &repo_url])
.with_exec(vec![ .with_exec(vec![
"cuddle-please", "cuddle-please",
@@ -257,10 +251,7 @@ impl DaggerCuddlePlease {
}, },
]); ]);
let exit_code = res.exit_code().await?; res.sync().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
let please_out = res.stdout().await?; let please_out = res.stdout().await?;
println!("{please_out}"); println!("{please_out}");
@@ -269,38 +260,26 @@ impl DaggerCuddlePlease {
Ok(()) Ok(())
} }
pub async fn cuddle_please_src( pub async fn cuddle_please_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
&self, let build_image = self.client.container().from(&args.cuddle_image);
client: Arc<dagger_sdk::Query>,
args: &CuddlePleaseSrcArgs,
) -> eyre::Result<()> {
let build_image = client.container().from(&args.cuddle_image);
let res = build_image let res = build_image
.with_secret_variable( .with_secret_variable(
"CUDDLE_PLEASE_TOKEN", "CUDDLE_PLEASE_TOKEN",
client self.client.set_secret(
.set_secret( "CUDDLE_PLEASE_TOKEN",
"CUDDLE_PLEASE_TOKEN", match &args.server {
match &args.server { SrcServer::Gitea { token, .. } => token,
SrcServer::Gitea { token, .. } => token, SrcServer::GitHub { token } => token,
SrcServer::GitHub { token } => token, },
}, ),
)
.id()
.await?,
) )
.with_workdir("/mnt/app") .with_workdir("/mnt/app")
.with_directory(".", client.host().directory(".").id().await?) .with_directory(".", self.client.host().directory("."))
.with_unix_socket( .with_unix_socket(
"/tmp/ssh.sock", "/tmp/ssh.sock",
client self.client.host().unix_socket(
.host() std::env::var("SSH_AUTH_SOCK").expect("expect SSH_AUTH_SOCK to be present"),
.unix_socket( ),
std::env::var("SSH_AUTH_SOCK").expect("expect SSH_AUTH_SOCK to be present"),
)
.id()
.await?,
) )
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh.sock") .with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh.sock")
.with_new_file_opts( .with_new_file_opts(
@@ -309,6 +288,7 @@ impl DaggerCuddlePlease {
contents: Some( contents: Some(
" "
Host * Host *
User git
StrictHostKeyChecking no StrictHostKeyChecking no
UserKnownHostsFile /dev/null UserKnownHostsFile /dev/null
", ",
@@ -316,31 +296,48 @@ Host *
owner: Some("root"), owner: Some("root"),
permissions: Some(700), permissions: Some(700),
}, },
) );
.with_exec(vec![
"cuddle-please",
"release",
&format!(
"--engine={}",
match &args.server {
SrcServer::Gitea { .. } => "gitea",
SrcServer::GitHub { .. } => "github",
}
),
"--log-level",
match args.log_level.as_ref().unwrap_or(&LogLevel::Info) {
LogLevel::Trace => "trace",
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Warn => "warn",
LogLevel::Error => "error",
},
]);
let exit_code = res.exit_code().await?; let remote_url = res
if exit_code != 0 { .with_exec(vec!["git", "config", "--get", "remote.origin.url"])
eyre::bail!("failed to run cuddle-please"); .stdout()
} .await?;
let res = if remote_url.starts_with("http") {
let new_remote_url = format!(
"ssh://git@{}",
remote_url
.trim()
.trim_start_matches("https://")
.trim_start_matches("http://")
);
println!("new remote_url: {}", new_remote_url);
res.with_exec(vec!["git", "remote", "set-url", "origin", &new_remote_url])
} else {
res
};
let res = res.with_exec(vec![
"cuddle-please",
"release",
&format!(
"--engine={}",
match &args.server {
SrcServer::Gitea { .. } => "gitea",
SrcServer::GitHub { .. } => "github",
}
),
"--log-level",
match args.log_level.as_ref().unwrap_or(&LogLevel::Info) {
LogLevel::Trace => "trace",
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Warn => "warn",
LogLevel::Error => "error",
},
]);
res.sync().await?;
let please_out = res.stdout().await?; let please_out = res.stdout().await?;
println!("{please_out}"); println!("{please_out}");

View File

@@ -0,0 +1,16 @@
[package]
name = "dagger-leptos"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,19 @@
[package]
name = "dagger-rust"
description = "A common set of components for dagger-sdk, which enables patterns such as build, test and publish"
version.workspace = true
edition.workspace = true
authors.workspace = true
readme.workspace = true
license.workspace = true
repository.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,363 @@
use std::path::PathBuf;
use crate::source::RustSource;
#[allow(dead_code)]
pub struct RustBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl RustBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
target: impl AsRef<BuildTarget>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let rust_version = rust_version.as_ref();
let target = target.as_ref();
let profile = profile.as_ref();
let source_path = source_path.map(|s| s.into());
let source = source_path.clone().unwrap_or(PathBuf::from("."));
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(source_path, crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", &target.to_string()])
.with_exec(vec!["apt", "update"])
.with_exec(deps);
let target_cache = self.client.cache_volume(format!(
"rust_target_{}_{}",
profile.to_string(),
target.to_string()
));
let target_str = target.to_string();
let mut build_options = vec!["cargo", "build", "--target", &target_str, "--workspace"];
if matches!(profile, BuildProfile::Release) {
build_options.push("--release");
}
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let mut containers = Vec::new();
for container_image in images {
let container = match &container_image {
SlimImage::Debian { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_debian_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
id: None,
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_file(format!("/usr/local/bin/{}", bin_name), bin)
.with_exec(vec![bin_name, "--help"]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
id: None,
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}
pub enum RustVersion {
Nightly,
Stable(String),
}
impl AsRef<RustVersion> for RustVersion {
fn as_ref(&self) -> &RustVersion {
self
}
}
impl ToString for RustVersion {
fn to_string(&self) -> String {
match self {
RustVersion::Nightly => "rustlang/rust:nightly".to_string(),
RustVersion::Stable(version) => format!("rust:{}", version),
}
}
}
pub enum BuildTarget {
LinuxAmd64,
LinuxArm64,
LinuxAmd64Musl,
LinuxArm64Musl,
MacOSAmd64,
MacOSArm64,
}
impl BuildTarget {
pub fn from_target(image: &SlimImage) -> Self {
match image {
SlimImage::Debian { architecture, .. } => match architecture {
BuildArchitecture::Amd64 => Self::LinuxAmd64,
BuildArchitecture::Arm64 => Self::LinuxArm64,
},
SlimImage::Alpine { architecture, .. } => match architecture {
BuildArchitecture::Amd64 => Self::LinuxAmd64Musl,
BuildArchitecture::Arm64 => Self::LinuxArm64Musl,
},
}
}
pub fn into_platform(&self) -> dagger_sdk::Platform {
let platform = match self {
BuildTarget::LinuxAmd64 => "linux/amd64",
BuildTarget::LinuxArm64 => "linux/arm64",
BuildTarget::LinuxAmd64Musl => "linux/amd64",
BuildTarget::LinuxArm64Musl => "linux/arm64",
BuildTarget::MacOSAmd64 => "darwin/amd64",
BuildTarget::MacOSArm64 => "darwin/arm64",
};
dagger_sdk::Platform(platform.into())
}
}
impl AsRef<BuildTarget> for BuildTarget {
fn as_ref(&self) -> &BuildTarget {
self
}
}
impl ToString for BuildTarget {
fn to_string(&self) -> String {
let target = match self {
BuildTarget::LinuxAmd64 => "x86_64-unknown-linux-gnu",
BuildTarget::LinuxArm64 => "aarch64-unknown-linux-gnu",
BuildTarget::LinuxAmd64Musl => "x86_64-unknown-linux-musl",
BuildTarget::LinuxArm64Musl => "aarch64-unknown-linux-musl",
BuildTarget::MacOSAmd64 => "x86_64-apple-darwin",
BuildTarget::MacOSArm64 => "aarch64-apple-darwin",
};
target.into()
}
}
pub enum BuildProfile {
Debug,
Release,
}
impl AsRef<BuildProfile> for BuildProfile {
fn as_ref(&self) -> &BuildProfile {
self
}
}
impl ToString for BuildProfile {
fn to_string(&self) -> String {
let profile = match self {
BuildProfile::Debug => "debug",
BuildProfile::Release => "release",
};
profile.into()
}
}
pub enum SlimImage {
Debian {
image: String,
deps: Vec<String>,
architecture: BuildArchitecture,
},
Alpine {
image: String,
deps: Vec<String>,
architecture: BuildArchitecture,
},
}
pub enum BuildArchitecture {
Amd64,
Arm64,
}

View File

@@ -0,0 +1,266 @@
use std::path::PathBuf;
use crate::{
build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
source::RustSource,
};
pub struct HtmxBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl HtmxBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let rust_version = rust_version.as_ref();
let profile = profile.as_ref();
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(Some(&source_path), crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
.with_exec(vec!["apt", "update"])
.with_exec(deps)
.with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
.with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
.with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
.with_exec(vec!["wget", "https://github.com/rui314/mold/releases/latest/download/mold-2.3.3-x86_64-linux.tar.gz"])
.with_exec("tar -xvf mold-2.3.3-x86_64-linux.tar.gz".split_whitespace().collect())
.with_exec("mv mold /usr/bin/mold".split_whitespace().collect())
.with_exec(vec!["cargo", "binstall", "sqlx-cli", "-y"]);
let target_cache = self
.client
.cache_volume(format!("rust_htmx_{}", profile.to_string()));
let build_options = vec!["cargo", "sqlx", "prepare"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let postgres_password = "somepassword123";
let postgres = self
.client
.container()
.from("postgres:16.1")
.with_env_variable("POSTGRES_PASSWORD", postgres_password);
let postgres_service = postgres.with_exposed_port(5432);
let mut containers = Vec::new();
for container_image in images {
let container =
match &container_image {
SlimImage::Debian { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let binary_build = build_container
.with_service_binding("postgres", postgres_service.as_service())
.with_env_variable(
"DATABASE_URL",
"root:somepassword123@postgres:5432/postgres",
)
.with_exec(vec!["cargo", "sqlx", "migrate", "run"])
.with_exec(vec!["cargo", "sqlx", "prepare"])
.with_exec(vec!["cargo", "build", "--release", "--bin", bin_name]);
self.build_debian_image(
binary_build,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!("target/{}/release/{}", target.to_string(), bin_name));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
builder_image: dagger_sdk::Container,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
id: None,
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_workdir("/mnt/app")
.with_file(
format!("/mnt/app/{bin_name}"),
builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
)
.with_directory(
"/mnt/app/target/site",
builder_image.directory(format!("/mnt/src/target/site")),
)
.with_file(
"/mnt/app/Cargo.toml",
builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
)
.with_env_variable("RUST_LOG", "debug")
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_env_variable("LEPTOS_SITE_ROOT", "site")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(8080)
.with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
id: None,
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}

View File

@@ -0,0 +1,245 @@
use std::path::PathBuf;
use crate::{
build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
source::RustSource,
};
pub struct LeptosBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl LeptosBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let rust_version = rust_version.as_ref();
let profile = profile.as_ref();
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(Some(&source_path), crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
.with_exec(vec!["apt", "update"])
.with_exec(deps)
.with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
.with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
.with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
.with_exec(vec!["cargo", "binstall", "cargo-leptos", "-y"]);
let target_cache = self
.client
.cache_volume(format!("rust_leptos_{}", profile.to_string()));
let mut build_options = vec!["cargo", "leptos", "build", "--release", "-vv"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let mut containers = Vec::new();
for container_image in images {
let container = match &container_image {
SlimImage::Debian { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let binary_build =
build_container
.with_exec(vec!["cargo", "leptos", "build", "--release", "-vv"]);
self.build_debian_image(
binary_build,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!("target/{}/release/{}", target.to_string(), bin_name));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
builder_image: dagger_sdk::Container,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
id: None,
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_workdir("/mnt/app")
.with_file(
format!("/mnt/app/{bin_name}"),
builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
)
.with_directory(
"/mnt/app/target/site",
builder_image.directory(format!("/mnt/src/target/site")),
)
.with_file(
"/mnt/app/Cargo.toml",
builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
)
.with_env_variable("RUST_LOG", "debug")
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_env_variable("LEPTOS_SITE_ROOT", "site")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(8080)
.with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
id: None,
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}

View File

@@ -0,0 +1,6 @@
pub mod build;
pub mod htmx;
pub mod leptos;
pub mod publish;
pub mod source;
pub mod test;

View File

@@ -0,0 +1,40 @@
use std::sync::Arc;
pub struct RustPublish {
client: Arc<dagger_sdk::Query>,
}
impl RustPublish {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
Self { client }
}
pub async fn publish(
&self,
image: impl Into<String>,
tag: impl Into<String>,
containers: impl IntoIterator<Item = dagger_sdk::Container>,
) -> eyre::Result<()> {
let mut ids = Vec::new();
for container in containers.into_iter() {
let id = container.id().await?;
ids.push(id);
}
let image = self
.client
.container()
.publish_opts(
format!("{}:{}", image.into(), tag.into()),
dagger_sdk::ContainerPublishOpts {
platform_variants: Some(ids),
forced_compression: None,
media_types: None,
},
)
.await?;
println!("published: {}", image);
Ok(())
}
}

View File

@@ -0,0 +1,200 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use eyre::Context;
pub struct RustSource {
client: dagger_sdk::Query,
exclude: Vec<String>,
}
impl RustSource {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
exclude: vec!["node_modules/", ".git/", "target/", ".cuddle/"]
.into_iter()
.map(|s| s.to_string())
.collect(),
}
}
pub fn with_exclude(
&mut self,
exclude: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.exclude = exclude.into_iter().map(|s| s.into()).collect();
self
}
pub fn append_exclude(
&mut self,
exclude: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.exclude
.append(&mut exclude.into_iter().map(|s| s.into()).collect::<Vec<_>>());
self
}
pub async fn get_rust_src<T, I>(
&self,
source: Option<T>,
crate_paths: I,
) -> eyre::Result<(dagger_sdk::Directory, dagger_sdk::Directory)>
where
T: Into<PathBuf>,
T: Clone,
I: IntoIterator,
I::Item: Into<String>,
{
let source_path = match source.clone() {
Some(s) => s.into(),
None => PathBuf::from("."),
};
let (skeleton_files, _crates) = self
.get_rust_skeleton_files(&source_path, crate_paths)
.await?;
let src = self.get_src(source.clone()).await?;
let rust_src = self.get_rust_dep_src(source).await?;
let rust_src = rust_src.with_directory(".", skeleton_files);
Ok((src, rust_src))
}
pub async fn get_src(
&self,
source: Option<impl Into<PathBuf>>,
) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let directory = self.client.host().directory_opts(
source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(self.exclude.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?,
);
Ok(directory)
}
pub async fn get_rust_dep_src(
&self,
source: Option<impl Into<PathBuf>>,
) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let mut excludes = self.exclude.clone();
excludes.push("**/src".to_string());
let directory = self.client.host().directory_opts(
source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
//.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.exclude(excludes.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?,
);
Ok(directory)
}
pub async fn get_rust_target_src(
&self,
source_path: &Path,
container: dagger_sdk::Container,
crate_paths: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<dagger_sdk::Directory> {
let (_skeleton_files, crates) = self
.get_rust_skeleton_files(source_path, crate_paths)
.await?;
let exclude = crates
.iter()
.map(|c| format!("**/*{}*", c.replace('-', "_")))
.collect::<Vec<_>>();
let exclude = exclude.iter().map(|c| c.as_str()).collect();
let incremental_dir = self.client.directory().with_directory_opts(
".",
container.directory("target"),
dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude),
include: None,
},
);
return Ok(incremental_dir);
}
pub async fn get_rust_skeleton_files(
&self,
source_path: &Path,
crate_paths: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
let paths = crate_paths
.into_iter()
.map(|s| s.into())
.collect::<Vec<String>>();
let mut crates = Vec::new();
for path in paths {
if path.ends_with("/*") {
let mut dirs = tokio::fs::read_dir(source_path.join(path.trim_end_matches("/*")))
.await
.context(format!("failed to find path: {}", path.clone()))?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
crates.push(entry.path());
}
}
} else {
crates.push(PathBuf::from(path));
}
}
fn create_skeleton_files(
directory: dagger_sdk::Directory,
path: &Path,
) -> eyre::Result<dagger_sdk::Directory> {
let main_content = r#"
#[allow(dead_code)]
fn main() { panic!("should never be executed"); }"#;
let lib_content = r#"
#[allow(dead_code)]
fn some() { panic!("should never be executed"); }"#;
let directory = directory.with_new_file(
path.join("src").join("main.rs").display().to_string(),
main_content,
);
let directory = directory.with_new_file(
path.join("src").join("lib.rs").display().to_string(),
lib_content,
);
Ok(directory)
}
let mut directory = self.client.directory();
let mut crate_names = Vec::new();
for rust_crate in crates.iter() {
if let Some(file_name) = rust_crate.file_name() {
crate_names.push(file_name.to_str().unwrap().to_string());
}
directory = create_skeleton_files(
directory,
rust_crate.strip_prefix(source_path).unwrap_or(&rust_crate),
)?;
}
Ok((directory, crate_names))
}
}

View File

@@ -0,0 +1,75 @@
use std::{path::PathBuf, sync::Arc};
use crate::{build::RustVersion, source::RustSource};
pub struct RustTest {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl RustTest {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn test(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<()> {
let rust_version = rust_version.as_ref();
let source_path = source_path.map(|s| s.into());
let source = source_path.clone().unwrap_or(PathBuf::from("."));
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(source_path, crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["apt", "update"])
.with_exec(deps);
let target_cache = self.client.cache_volume(format!("rust_target_test",));
let build_options = vec!["cargo", "build", "--workspace"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let test = rust_with_src.with_exec(vec!["cargo", "test"]);
let stdout = test.stdout().await?;
let stderr = test.stderr().await?;
println!("stdout: {}, stderr: {}", stdout, stderr);
test.sync().await?;
Ok(())
}
}

View File

@@ -6,6 +6,8 @@ vars:
service: "dagger-components" service: "dagger-components"
registry: kasperhermansen registry: kasperhermansen
cuddle_please_image: "kasperhermansen/cuddle-please:main-1691504183"
please: please:
project: project:
owner: kjuulh owner: kjuulh

View File

@@ -4,7 +4,7 @@ use dagger_cuddle_please::{models::CuddlePleaseArgs, DaggerCuddlePleaseAction};
pub async fn main() -> eyre::Result<()> { pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?; let client = dagger_sdk::connect().await?;
DaggerCuddlePleaseAction::dagger(client.clone()) DaggerCuddlePleaseAction::dagger(client)
.execute(&CuddlePleaseArgs { .execute(&CuddlePleaseArgs {
repository: "dagger-components".into(), repository: "dagger-components".into(),
owner: "kjuulh".into(), owner: "kjuulh".into(),

17
examples/htmx/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "htmx"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

16
examples/htmx/src/main.rs Normal file
View File

@@ -0,0 +1,16 @@
use std::path::PathBuf;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
let crates = ["some-crate"];
let dag = dagger_rust::source::RustSource::new(client.clone());
let (_src, _rust_src) = dag.get_rust_src(None::<PathBuf>, crates).await?;
let _full_src = dag
.get_rust_target_src(&PathBuf::from("."), client.container(), crates)
.await?;
Ok(())
}

View File

@@ -0,0 +1,13 @@
[package]
name = "leptos-build"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,97 @@
[package]
name = "hackernews_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { version = "0.5.1", features = ["nightly"] }
leptos_axum = { version = "0.5.1", optional = true }
leptos_meta = { version = "0.5.1", features = ["nightly"] }
leptos_router = { version = "0.5.1", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

Binary file not shown.

View File

@@ -0,0 +1,62 @@
use dagger_rust::build::{BuildProfile, RustVersion, SlimImage};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
let rust_build = dagger_rust::leptos::LeptosBuild::new(client.clone());
let containers = rust_build
.build_release(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&[
"openssl",
"libssl-dev",
"pkg-config",
"musl-tools",
"ca-certificates",
],
vec![SlimImage::Debian {
image: "debian:bullseye".into(),
deps: vec![
"openssl".into(),
"libssl-dev".into(),
"pkg-config".into(),
"musl-tools".into(),
"ca-certificates".into(),
],
architecture: dagger_rust::build::BuildArchitecture::Amd64,
}],
"hackernews_axum",
)
.await?;
let container = containers.first().unwrap();
container.directory("/mnt/app").export("output").await?;
let tunnel = client.host().tunnel(
container
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_exec(vec!["/mnt/app/hackernews_axum"])
.as_service(),
);
tunnel.start().await?;
let endpoint = tunnel
.endpoint_opts(
dagger_sdk::ServiceEndpointOptsBuilder::default()
.scheme("http")
.build()?,
)
.await?;
println!("running on: {endpoint}, press enter to stop");
std::io::stdin().read_line(&mut String::new()).unwrap();
Ok(())
}

2809
examples/leptos-build/testdata/Cargo.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[profile.release]
codegen-units = 1
lto = true
[[workspace.metadata.leptos]]
name = "hackernews_axum"
bin-package = "hackernews_axum"
lib-package = "hackernews_axum"
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./crates/hackernews_axum/style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "./crates/hackernews_axum/public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,97 @@
[package]
name = "hackernews_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { version = "0.5.1", features = ["nightly"] }
leptos_axum = { version = "0.5.1", optional = true }
leptos_meta = { version = "0.5.1", features = ["nightly"] }
leptos_router = { version = "0.5.1", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,8 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "hackernews_axum"

View File

@@ -0,0 +1,7 @@
# Leptos Hacker News Example with Axum
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,90 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
format!("https://node-hnapi.herokuapp.com/{path}")
}
pub fn user(path: &str) -> String {
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
// abort in-flight requests if e.g., we've navigated away from this page
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = reqwest::get(path)
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Story {
pub id: usize,
pub title: String,
pub points: Option<i32>,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
#[serde(alias = "type")]
pub story_type: String,
pub url: String,
#[serde(default)]
pub domain: String,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@@ -0,0 +1,28 @@
use leptos::{view, Errors, For, IntoView, RwSignal, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children=move | (_, error)| {
let error_string = error.to_string();
view! {
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view()
}

View File

@@ -0,0 +1,44 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}
}

View File

@@ -0,0 +1,63 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -0,0 +1,49 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
</>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
}

View File

@@ -0,0 +1,54 @@
use cfg_if::cfg_if;
use leptos::{logging::log, *};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use hackernews_axum::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@@ -0,0 +1,30 @@
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@@ -0,0 +1,156 @@
use crate::api;
use leptos::*;
use leptos_router::*;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
}
#[component]
pub fn Stories() -> impl IntoView {
let query = use_query_map();
let params = use_params_map();
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
>
{move || match stories.get() {
None => None,
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
</ul>
}.into_any())
}
}}
</Transition>
</div>
</main>
</div>
}
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
view! {
<li class="news-item">
<span class="score">{story.points}</span>
<span class="title">
{if !story.url.starts_with("item?id=") {
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
}

View File

@@ -0,0 +1,125 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
let meta_description = move || {
story
.get()
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
</div>
</div>
}})}
</Suspense>
}
}
#[component]
pub fn Comment(comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(true);
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{
let comments_len = comment.comments.len();
move || if open() {
"[-]".into()
} else {
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
}
}
</a>
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
}
})}
</div>
}
})}
</li>
}
}
fn pluralize(n: usize) -> &'static str {
if n == 1 {
" reply"
} else {
" replies"
}
}

View File

@@ -0,0 +1,46 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| view! { "Loading..." }>
{move || user.get().map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_any(),
Some(user) => view! {
<div>
<h1>"User: " {&user.id}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { <li inner_html=about class="about"></li> })}
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}.into_any()
})}
</Suspense>
</div>
}
}

View File

@@ -0,0 +1,326 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 15px;
background-color: #f2f3f5;
margin: 0;
padding-top: 55px;
color: #34495e;
overflow-y: scroll
}
a {
color: #34495e;
text-decoration: none
}
.header {
background-color: #335d92;
position: fixed;
z-index: 999;
height: 55px;
top: 0;
left: 0;
right: 0
}
.header .inner {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
padding: 15px 5px
}
.header a {
color: rgba(255, 255, 255, .8);
line-height: 24px;
transition: color .15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: .075em;
margin-right: 1.8em
}
.header a:hover {
color: #fff
}
.header a.active {
color: #fff;
font-weight: 400
}
.header a:nth-child(6) {
margin-right: 0
}
.header .github {
color: #fff;
font-size: .9em;
margin: 0;
float: right
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative
}
.fade-enter-active,
.fade-exit-active {
transition: all .2s ease
}
.fade-enter,
.fade-exit-active {
opacity: 0
}
@media (max-width:860px) {
.header .inner {
padding: 15px 30px
}
}
@media (max-width:600px) {
.header .inner {
padding: 15px
}
.header a {
margin-right: 1em
}
.header .github {
display: none
}
}
.news-view {
padding-top: 45px
}
.news-list,
.news-list-nav {
background-color: #fff;
border-radius: 2px
}
.news-list-nav {
padding: 15px 30px;
position: fixed;
text-align: center;
top: 55px;
left: 0;
right: 0;
z-index: 998;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.news-list-nav .page-link {
margin: 0 1em
}
.news-list-nav .disabled {
color: #aaa
}
.news-list {
position: absolute;
margin: 30px 0;
width: 100%;
transition: all .5s cubic-bezier(.55, 0, .1, 1)
}
.news-list ul {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.news-list {
margin: 10px 0
}
}
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px
}
.news-item .score {
color: #335d92;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px
}
.news-item .host,
.news-item .meta {
font-size: .85em;
color: #626262
}
.news-item .host a,
.news-item .meta a {
color: #626262;
text-decoration: underline
}
.news-item .host a:hover,
.news-item .meta a:hover {
color: #335d92
}
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.item-view-header h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: .5em
}
.item-view-header .host,
.item-view-header .meta,
.item-view-header .meta a {
color: #626262
}
.item-view-header .meta a {
text-decoration: underline
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em .5em
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative
}
.item-view-comments-header .spinner {
display: inline-block;
margin: -15px 0
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.item-view-header h1 {
font-size: 1.25em
}
}
.comment-children .comment-children {
margin-left: 1.5em
}
.comment {
border-top: 1px solid #eee;
position: relative
}
.comment .by,
.comment .text,
.comment .toggle {
font-size: .9em;
margin: 1em 0
}
.comment .by {
color: #626262
}
.comment .by a {
color: #626262;
text-decoration: underline
}
.comment .text {
overflow-wrap: break-word
}
.comment .text a:hover {
color: #335d92
}
.comment .text pre {
white-space: pre-wrap
}
.comment .toggle {
background-color: #fffbf2;
padding: .3em .5em;
border-radius: 4px
}
.comment .toggle a {
color: #626262;
cursor: pointer
}
.comment .toggle.open {
padding: 0;
background-color: transparent;
margin-bottom: -.5em
}
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em
}
.user-view h1 {
margin: 0;
font-size: 1.5em
}
.user-view .meta {
list-style-type: none;
padding: 0
}
.user-view .label {
display: inline-block;
min-width: 4em
}
.user-view .about {
margin: 1em 0
}
.user-view .links a {
text-decoration: underline
}

View File

@@ -0,0 +1,13 @@
[package]
name = "rust-build"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,29 @@
use dagger_rust::build::{RustVersion, SlimImage};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
let rust_build = dagger_rust::build::RustBuild::new(client.clone());
let containers = rust_build
.build_release(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&["openssl"],
vec![SlimImage::Debian {
image: "debian:bookworm".into(),
deps: vec!["openssl".into()],
architecture: dagger_rust::build::BuildArchitecture::Amd64,
}],
"example_bin",
)
.await?;
for container in containers {
container.sync().await?;
}
Ok(())
}

View File

@@ -0,0 +1,3 @@
[workspace]
members = ["crates/*"]
resolver = "2"

View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,8 @@
[package]
name = "example_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View File

@@ -0,0 +1,13 @@
[package]
name = "rust-src"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,16 @@
use std::path::PathBuf;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
let crates = ["some-crate"];
let dag = dagger_rust::source::RustSource::new(client.clone());
let (_src, _rust_src) = dag.get_rust_src(None::<PathBuf>, crates).await?;
let _full_src = dag
.get_rust_target_src(&PathBuf::from("."), client.container(), crates)
.await?;
Ok(())
}

View File

@@ -0,0 +1,13 @@
[package]
name = "rust-test"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,16 @@
use dagger_rust::{build::RustVersion, test::RustTest};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
RustTest::new(client.clone())
.test(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&["openssl"],
)
.await?;
Ok(())
}

7
examples/rust-test/testdata/Cargo.lock generated vendored Normal file
View File

@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "example_bin"
version = "0.1.0"

View File

@@ -0,0 +1,3 @@
[workspace]
members = ["crates/*"]
resolver = "2"

View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,8 @@
[package]
name = "example_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,11 @@
fn main() {
println!("Hello, world!");
}
#[cfg(test)]
mod tests {
#[test]
fn test_main() {
assert_eq!(1, 1)
}
}

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -9,4 +9,4 @@ if [[ -n "$CI_PREFIX" ]]; then
fi fi
$CMD_PREFIX main $CMD_PREFIX main --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"

View File

@@ -8,4 +8,4 @@ if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX" CMD_PREFIX="$CI_PREFIX"
fi fi
$CMD_PREFIX pull-request $CMD_PREFIX pull-request --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"

11
scripts/ci:release.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
CMD_PREFIX="cargo run -p ci --"
if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX"
fi
$CMD_PREFIX pull-request --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"