31 Commits

Author SHA1 Message Date
7a1ad63b57 feat: with full support for rust services
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-26 22:19:34 +01:00
80782e70f9 chore: fmt
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 23:16:21 +01:00
3939940c01 chore: fmt
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 23:14:38 +01:00
82ccdefd93 feat: with middleware
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 23:10:09 +01:00
3e9a840851 chore(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-25 21:26:21 +00:00
455660f1e0 chore(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-11-25 21:01:05 +00:00
f5ba46186b feat: with logs
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 21:49:04 +01:00
8cf148726a feat: add cuddle ci draft
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 21:41:17 +01:00
e29615cb05 feat: with offline mode
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 14:00:17 +01:00
cdd13283e0 feat: with cargo clean
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 13:50:43 +01:00
a3d92cdde3 feat: without export
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 13:47:17 +01:00
e4fc1cc834 feat: with output
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 13:33:54 +01:00
d6af354776 feat: with nested mold
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:55:56 +01:00
0524b2e0bf feat: fix name
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-11-25 12:48:57 +01:00
5c69c3fa16 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:47:43 +01:00
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
56 changed files with 6227 additions and 471 deletions

596
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,14 @@
members = ["crates/*", "examples/*", "ci"] members = ["crates/*", "examples/*", "ci"]
resolver = "2" resolver = "2"
[workspace.package]
version = "0.2.0"
edition = "2021"
license = "MIT"
authors = ["kjuulh <contact@kjuulh.io>"]
readme = "README.md"
repository = "https://git.front.kjuulh.io/kjuulh/dagger-components"
[workspace.dependencies] [workspace.dependencies]
cuddle-components = { path = "crates/cuddle-components" } cuddle-components = { path = "crates/cuddle-components" }
dagger-components = { path = "crates/dagger-components" } dagger-components = { path = "crates/dagger-components" }
@@ -9,8 +17,13 @@ dagger-cuddle-please = { path = "crates/dagger-cuddle-please" }
dagger-rust = { path = "crates/dagger-rust" } dagger-rust = { path = "crates/dagger-rust" }
ci = { path = "ci" } ci = { path = "ci" }
dagger-sdk = "0.2.22" #dagger-sdk = "0.3.2"
eyre = "0.6.8" dagger-sdk = {git = "https://github.com/kjuulh/dagger.git", branch = "feat/with-send-sync"}
tokio = "1.31.0" eyre = "0.6.9"
tokio = "1.34.0"
dotenv = "0.15.0" dotenv = "0.15.0"
async-trait = "0.1.73" async-trait = "0.1.74"
color-eyre = "*"
clap = {version = "4", features = ["derive"]}
futures = "0.3.29"
async-scoped = { version = "0.8.0", features = ["tokio", "use-tokio"] }

View File

@@ -8,12 +8,12 @@ edition = "2021"
[dependencies] [dependencies]
dagger-cuddle-please.workspace = true dagger-cuddle-please.workspace = true
dagger-rust.workspace = true dagger-rust.workspace = true
dagger-sdk.workspace = true
dagger-sdk = "*"
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,5 +1,4 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use clap::Args; use clap::Args;
use clap::Parser; use clap::Parser;
@@ -62,10 +61,10 @@ async fn main() -> eyre::Result<()> {
let _ = dotenv::dotenv(); let _ = dotenv::dotenv();
let _ = color_eyre::install(); let _ = color_eyre::install();
let client = dagger_sdk::connect().await?;
let cli = Command::parse(); let cli = Command::parse();
let client = dagger_sdk::connect().await?;
match &cli.commands { match &cli.commands {
Commands::Local { command } => match command { Commands::Local { command } => match command {
LocalCommands::Test => { LocalCommands::Test => {
@@ -74,7 +73,7 @@ async fn main() -> eyre::Result<()> {
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;
test::execute(client.clone(), args).await.unwrap(); test::execute(client.clone(), args).await.unwrap();
@@ -83,13 +82,13 @@ async fn main() -> eyre::Result<()> {
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;
test::execute(client.clone(), args).await.unwrap(); test::execute(client.clone(), args).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();
@@ -100,21 +99,20 @@ async fn main() -> eyre::Result<()> {
cuddle_please(client.clone(), &cli) cuddle_please(client.clone(), &cli)
); );
} }
Commands::Release => todo!(), Commands::Release => {}
} }
Ok(()) Ok(())
} }
mod please_release { mod please_release {
use std::sync::Arc;
use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePleaseAction}; 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<()> {
DaggerCuddlePleaseAction::dagger(client) DaggerCuddlePleaseAction::dagger(client)
@@ -136,13 +134,13 @@ mod please_release {
} }
mod test { mod test {
use std::{path::PathBuf, sync::Arc}; use std::path::PathBuf;
use dagger_rust::build::RustVersion; use dagger_rust::build::RustVersion;
use crate::GlobalArgs; use crate::GlobalArgs;
pub async fn execute(client: Arc<dagger_sdk::Query>, _args: &GlobalArgs) -> eyre::Result<()> { pub async fn execute(client: dagger_sdk::Query, _args: &GlobalArgs) -> eyre::Result<()> {
dagger_rust::test::RustTest::new(client) dagger_rust::test::RustTest::new(client)
.test( .test(
None::<PathBuf>, None::<PathBuf>,

View File

@@ -0,0 +1,22 @@
[package]
name = "cuddle-ci"
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
dagger-sdk.workspace = true
eyre.workspace = true
clap.workspace = true
async-trait.workspace = true
futures.workspace = true
[dev-dependencies]
tokio.workspace = true

156
crates/cuddle-ci/src/cli.rs Normal file
View File

@@ -0,0 +1,156 @@
use std::{sync::Arc};
use async_trait::async_trait;
pub struct CuddleCI {
pr_action: Arc<dyn PullRequestAction + Send + Sync>,
main_action: Arc<dyn MainAction + Send + Sync>,
release_action: Arc<dyn ReleaseAction + Send + Sync>,
}
impl CuddleCI {
pub fn new(
pr: Arc<dyn PullRequestAction + Send + Sync>,
main: Arc<dyn MainAction + Send + Sync>,
release: Arc<dyn ReleaseAction + Send + Sync>,
) -> Self {
Self {
pr_action: pr,
main_action: main,
release_action: release,
}
}
pub fn with_pull_request(&mut self, pr: Arc<dyn PullRequestAction + Send + Sync>) -> &mut Self {
self.pr_action = pr;
self
}
pub fn with_main(&mut self, main: Arc<dyn MainAction + Send + Sync>) -> &mut Self {
self.main_action = main;
self
}
pub fn with_release(&mut self, release: Arc<dyn ReleaseAction + Send + Sync>) -> &mut Self {
self.release_action = release;
self
}
pub async fn execute(&mut self, args: impl IntoIterator<Item = &str>) -> eyre::Result<()> {
let matches = clap::Command::new("cuddle-ci")
.about("is a wrapper around common CI actions")
.subcommand(clap::Command::new("pr"))
.subcommand(clap::Command::new("main"))
.subcommand(clap::Command::new("release"))
.subcommand_required(true)
.try_get_matches_from(args.into_iter())?;
match matches.subcommand() {
Some((name, args)) => match (name, args) {
("pr", _args) => {
eprintln!("starting pr validate");
self.pr_action.execute_pull_request().await?;
eprintln!("finished pr validate");
}
("main", _args) => {
eprintln!("starting main validate");
self.main_action.execute_main().await?;
eprintln!("finished main validate");
}
("release", _args) => {
eprintln!("starting release validate");
self.release_action.execute_release().await?;
eprintln!("finished release validate");
}
(command_name, _) => {
eyre::bail!("command is not recognized: {}", command_name)
}
},
None => eyre::bail!("command required a subcommand [pr, main, release] etc."),
}
Ok(())
}
}
impl Default for CuddleCI {
fn default() -> Self {
Self::new(
Arc::new(DefaultPullRequestAction {}),
Arc::new(DefaultMainAction {}),
Arc::new(DefaultReleaseAction {}),
)
}
}
#[async_trait]
pub trait PullRequestAction {
async fn execute_pull_request(&self) -> eyre::Result<()> {
eprintln!("validate pull request: noop");
Ok(())
}
}
pub struct DefaultPullRequestAction {}
#[async_trait]
impl PullRequestAction for DefaultPullRequestAction {}
#[async_trait]
pub trait MainAction {
async fn execute_main(&self) -> eyre::Result<()> {
eprintln!("validate main: noop");
Ok(())
}
}
pub struct DefaultMainAction {}
#[async_trait]
impl MainAction for DefaultMainAction {}
#[async_trait]
pub trait ReleaseAction {
async fn execute_release(&self) -> eyre::Result<()> {
eprintln!("validate release: noop");
Ok(())
}
}
pub struct DefaultReleaseAction {}
#[async_trait]
impl ReleaseAction for DefaultReleaseAction {}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn test_can_call_default() -> eyre::Result<()> {
CuddleCI::default().execute(["cuddle-ci", "pr"]).await?;
Ok(())
}
#[tokio::test]
async fn test_fails_on_no_command() -> eyre::Result<()> {
let res = CuddleCI::default().execute(["cuddle-ci"]).await;
assert!(res.is_err());
Ok(())
}
#[tokio::test]
async fn test_fails_on_wrong_command() -> eyre::Result<()> {
let res = CuddleCI::default()
.execute(["cuddle-ci", "something"])
.await;
assert!(res.is_err());
Ok(())
}
}

View File

@@ -0,0 +1,52 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use std::{future::Future, pin::Pin};
#[async_trait]
pub trait DaggerMiddleware {
async fn handle(
&self,
container: dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
Ok(container)
}
}
pub struct DaggerMiddlewareFn<F>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>>,
{
pub func: F,
}
pub fn middleware<F>(func: F) -> Box<DaggerMiddlewareFn<F>>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>>,
{
Box::new(DaggerMiddlewareFn { func })
}
#[async_trait]
impl<F> DaggerMiddleware for DaggerMiddlewareFn<F>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>> + Send + Sync,
{
async fn handle(&self, container: Container) -> eyre::Result<Container> {
// Call the closure stored in the struct
(self.func)(container).await
}
}
#[cfg(test)]
mod test {
use futures::FutureExt;
use super::*;
#[tokio::test]
async fn can_add_middleware() -> eyre::Result<()> {
middleware(|c| async move { Ok(c) }.boxed());
Ok(())
}
}

View File

@@ -0,0 +1,5 @@
pub mod cli;
pub use cli::*;
pub mod dagger_middleware;
pub mod rust_service;

View File

@@ -0,0 +1,344 @@
use std::path::PathBuf;
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use futures::{stream, StreamExt};
use crate::{dagger_middleware::DaggerMiddleware, MainAction, PullRequestAction};
pub type DynMiddleware = Box<dyn DaggerMiddleware + Send + Sync>;
pub enum RustServiceStage {
BeforeDeps(DynMiddleware),
AfterDeps(DynMiddleware),
BeforeBase(DynMiddleware),
AfterBase(DynMiddleware),
BeforeBuild(DynMiddleware),
AfterBuild(DynMiddleware),
BeforePackage(DynMiddleware),
AfterPackage(DynMiddleware),
BeforeRelease(DynMiddleware),
AfterRelease(DynMiddleware),
}
pub struct RustService {
client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
final_image: Option<dagger_sdk::Container>,
stages: Vec<RustServiceStage>,
source: Option<PathBuf>,
crates: Vec<String>,
bin_name: String,
}
impl From<dagger_sdk::Query> for RustService {
fn from(value: dagger_sdk::Query) -> Self {
Self {
client: value,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: String::new(),
}
}
}
impl RustService {
pub async fn new() -> eyre::Result<Self> {
Ok(Self {
client: dagger_sdk::connect().await?,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: String::new(),
})
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_bin_name(&mut self, bin_name: impl Into<String>) -> &mut Self {
self.bin_name = bin_name.into();
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
async fn run_stage(
&self,
stages: impl IntoIterator<Item = &Box<dyn DaggerMiddleware + Send + Sync>>,
container: Container,
) -> eyre::Result<Container> {
let before_deps_stream = stream::iter(stages.into_iter().map(Ok));
let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move {
match (base, m) {
(Ok(base), Ok(m)) => m.handle(base).await,
(_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"),
}
})
.await?;
Ok(res)
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let rust_src = RustSource::new(self.client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(self.client.container().from("rustlang/rust:nightly"));
let before_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeDeps(middleware) => Some(middleware),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_deps, base_image).await?;
let after_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterDeps(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_deps, image).await?;
let before_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_base, image).await?;
let cache = self.client.cache_volume("rust_target_cache");
let rust_prebuild = image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = 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 after_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_base, rust_with_src).await?;
Ok(image)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
let binary_build =
base.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name]);
let after_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let binary_build = self.run_stage(after_build, binary_build).await?;
let dest = self
.final_image
.clone()
.unwrap_or(self.client.container().from("debian:bullseye"));
let before_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforePackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let dest = self.run_stage(before_package, dest).await?;
let final_image = dest.with_workdir("/mnt/app").with_file(
format!("/usr/local/bin/{}", self.bin_name),
binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)),
);
let after_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterPackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let final_image = self.run_stage(after_package, final_image).await?;
Ok(final_image)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
base.with_exec(vec!["cargo", "test", "--release"])
.sync()
.await?;
Ok(())
}
}
#[async_trait]
impl PullRequestAction for RustService {
async fn execute_pull_request(&self) -> eyre::Result<()> {
let _container = self.build_release().await?;
Ok(())
}
}
#[async_trait]
impl MainAction for RustService {
async fn execute_main(&self) -> eyre::Result<()> {
Ok(())
}
}
pub mod architecture {
#[derive(Debug)]
pub enum Architecture {
Amd64,
Arm64,
}
#[derive(Debug)]
pub enum Os {
Linux,
MacOS,
}
}
pub mod apt;
pub mod cargo_binstall;
pub mod clap_sanity_test;
pub mod mold;
pub mod sqlx;
#[cfg(test)]
mod test {
use futures::FutureExt;
use crate::{
dagger_middleware::middleware,
rust_service::{
apt::AptExt,
architecture::{Architecture, Os},
cargo_binstall::CargoBInstallExt,
clap_sanity_test::ClapSanityTestExt,
mold::MoldActionExt,
RustService, RustServiceStage,
},
};
#[tokio::test]
async fn test_can_build_rust() -> eyre::Result<()> {
let client = dagger_sdk::connect().await?;
let root_dir = std::path::PathBuf::from("../../").canonicalize()?;
let container = RustService::from(client.clone())
.with_apt(&["git"])
.with_cargo_binstall(Architecture::Amd64, Os::Linux, "latest", ["sqlx-cli"])
.with_source(root_dir)
.with_bin_name("ci")
.with_crates(["crates/*", "examples/*", "ci"])
.with_mold(Architecture::Amd64, Os::Linux, "2.3.3")
.with_stage(RustServiceStage::BeforeDeps(middleware(|c| {
async move {
// Noop
Ok(c)
}
.boxed()
})))
.with_clap_sanity_test()
.build_release()
.await?;
container.sync().await?;
Ok(())
}
}

View File

@@ -0,0 +1,52 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct Apt {
deps: Vec<String>,
}
impl Apt {
pub fn new() -> Self {
Self { deps: Vec::new() }
}
pub fn add(mut self, dep_name: impl Into<String>) -> Self {
self.deps.push(dep_name.into());
self
}
pub fn extend(mut self, deps: &[&str]) -> Self {
self.deps.extend(deps.iter().map(|s| s.to_string()));
self
}
}
#[async_trait]
impl DaggerMiddleware for Apt {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let mut deps = vec!["apt", "install", "-y"];
deps.extend(self.deps.iter().map(|s| s.as_str()));
let c = container.with_exec(vec!["apt", "update"]).with_exec(deps);
Ok(c)
}
}
pub trait AptExt {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self
}
}
impl AptExt for RustService {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Box::new(Apt::new().extend(deps))))
}
}

View File

@@ -0,0 +1,109 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::{
architecture::{Architecture, Os},
RustService,
};
pub struct CargoBInstall {
arch: Architecture,
os: Os,
version: String,
crates: Vec<String>,
}
impl CargoBInstall {
pub fn new(
arch: Architecture,
os: Os,
version: impl Into<String>,
crates: impl Into<Vec<String>>,
) -> Self {
Self {
arch,
os,
version: version.into(),
crates: crates.into(),
}
}
fn get_arch(&self) -> String {
match self.arch {
Architecture::Amd64 => "x86_64",
Architecture::Arm64 => "armv7",
}
.into()
}
fn get_os(&self) -> String {
match self.os {
Os::Linux => "linux",
Os::MacOS => "darwin",
}
.into()
}
pub fn get_download_url(&self) -> String {
format!("https://github.com/cargo-bins/cargo-binstall/releases/{}/download/cargo-binstall-{}-unknown-{}-musl.tgz", self.version, self.get_arch(), self.get_os())
}
pub fn get_archive(&self) -> String {
format!(
"cargo-binstall-{}-unknown-{}-musl.tgz",
self.get_arch(),
self.get_os()
)
}
}
#[async_trait]
impl DaggerMiddleware for CargoBInstall {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let c =
container
.with_exec(vec!["wget", &self.get_download_url()])
.with_exec(vec!["tar", "-xvf", &self.get_archive()])
.with_exec(
"mv cargo-binstall /usr/local/cargo/bin"
.split_whitespace()
.collect(),
);
let c = self.crates.iter().cloned().fold(c, |acc, item| {
acc.with_exec(vec!["cargo", "binstall", &item, "-y"])
});
Ok(c)
}
}
pub trait CargoBInstallExt {
fn with_cargo_binstall(
&mut self,
arch: Architecture,
os: Os,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self
}
}
impl CargoBInstallExt for RustService {
fn with_cargo_binstall(
&mut self,
arch: Architecture,
os: Os,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
let crates: Vec<String> = crates.into_iter().map(|s| s.into()).collect();
self.with_stage(super::RustServiceStage::BeforeDeps(
Box::new(CargoBInstall::new(arch, os, version, crates))
))
}
}

View File

@@ -0,0 +1,41 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct ClapSanityTest {
bin_name: String,
}
impl ClapSanityTest {
pub fn new(bin_name: impl Into<String>) -> Self {
Self {
bin_name: bin_name.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for ClapSanityTest {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container.with_exec(vec![&self.bin_name, "--help"]))
}
}
pub trait ClapSanityTestExt {
fn with_clap_sanity_test(&mut self) -> &mut Self {
self
}
}
impl ClapSanityTestExt for RustService {
fn with_clap_sanity_test(&mut self) -> &mut Self {
self.with_stage(
super::RustServiceStage::AfterPackage(Box::new(ClapSanityTest::new(&self.bin_name)))
);
self
}
}

View File

@@ -0,0 +1,112 @@
use async_trait::async_trait;
use crate::dagger_middleware::DaggerMiddleware;
use super::{
architecture::{Architecture, Os},
RustService,
};
pub struct MoldInstall {
arch: Architecture,
os: Os,
version: String,
}
impl MoldInstall {
pub fn new(arch: Architecture, os: Os, version: impl Into<String>) -> Self {
Self {
arch,
os,
version: version.into(),
}
}
fn get_arch(&self) -> String {
match self.arch {
Architecture::Amd64 => "x86_64",
Architecture::Arm64 => "arm",
}
.into()
}
fn get_os(&self) -> String {
match &self.os {
Os::Linux => "linux",
o => todo!("os not implemented for mold: {:?}", o),
}
.into()
}
pub fn get_download_url(&self) -> String {
format!(
"https://github.com/rui314/mold/releases/download/v{}/mold-{}-{}-{}.tar.gz",
self.version,
self.version,
self.get_arch(),
self.get_os()
)
}
pub fn get_folder(&self) -> String {
format!(
"mold-{}-{}-{}",
self.version,
self.get_arch(),
self.get_os()
)
}
pub fn get_archive_name(&self) -> String {
format!(
"mold-{}-{}-{}.tar.gz",
self.version,
self.get_arch(),
self.get_os()
)
}
}
#[async_trait]
impl DaggerMiddleware for MoldInstall {
async fn handle(
&self,
container: dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
println!("installing mold");
let c = container
.with_exec(vec!["wget", &self.get_download_url()])
.with_exec(vec!["tar", "-xvf", &self.get_archive_name()])
.with_exec(vec![
"mv",
&format!("{}/bin/mold", self.get_folder()),
"/usr/bin/mold",
]);
Ok(c)
}
}
pub trait MoldActionExt {
fn with_mold(
&mut self,
architecture: Architecture,
os: Os,
version: impl Into<String>,
) -> &mut Self {
self
}
}
impl MoldActionExt for RustService {
fn with_mold(
&mut self,
architecture: Architecture,
os: Os,
version: impl Into<String>,
) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterDeps(
Box::new(MoldInstall::new(architecture, os, version))
))
}
}

View File

@@ -0,0 +1,12 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
pub struct Sqlx {}
#[async_trait]
impl DaggerMiddleware for Sqlx {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container.with_env_variable("SQLX_OFFLINE", "true"))
}
}

View File

@@ -1,7 +1,12 @@
[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

View File

@@ -1,4 +1,4 @@
use std::sync::Arc; use std::sync::{Arc};
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,37 +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(
@@ -357,10 +337,7 @@ Host *
}, },
]); ]);
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}");

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

@@ -1,7 +1,14 @@
[package] [package]
name = "dagger-rust" name = "dagger-rust"
version = "0.1.0" description = "A common set of components for dagger-sdk, which enables patterns such as build, test and publish"
edition = "2021" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,15 +1,15 @@
use std::{path::PathBuf, sync::Arc}; use std::path::PathBuf;
use crate::source::RustSource; use crate::source::RustSource;
#[allow(dead_code)] #[allow(dead_code)]
pub struct RustBuild { pub struct RustBuild {
client: Arc<dagger_sdk::Query>, client: dagger_sdk::Query,
registry: Option<String>, registry: Option<String>,
} }
impl RustBuild { impl RustBuild {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self { pub fn new(client: dagger_sdk::Query) -> Self {
Self { Self {
client, client,
registry: None, registry: None,
@@ -44,6 +44,9 @@ impl RustBuild {
.from(rust_version.to_string()) .from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", &target.to_string()]) .with_exec(vec!["rustup", "target", "add", &target.to_string()])
.with_exec(vec!["apt", "update"]) .with_exec(vec!["apt", "update"])
.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-2.3.3-x86_64-linux/bin/mold /usr/bin/mold".split_whitespace().collect())
.with_exec(deps); .with_exec(deps);
let target_cache = self.client.cache_volume(format!( let target_cache = self.client.cache_volume(format!(
@@ -60,9 +63,9 @@ impl RustBuild {
} }
let rust_prebuild = rust_build_image let rust_prebuild = rust_build_image
.with_workdir("/mnt/src") .with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src.id().await?) .with_directory("/mnt/src", dep_src)
.with_exec(build_options) .with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache.id().await?); .with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec()) .get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
@@ -72,10 +75,10 @@ impl RustBuild {
.with_workdir("/mnt/src") .with_workdir("/mnt/src")
.with_directory( .with_directory(
"/usr/local/cargo", "/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo").id().await?, rust_prebuild.directory("/usr/local/cargo"),
) )
.with_directory("/mnt/src/target", incremental_dir.id().await?) .with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src.id().await?); .with_directory("/mnt/src/", src);
Ok(rust_with_src) Ok(rust_with_src)
} }
@@ -94,92 +97,87 @@ impl RustBuild {
let mut containers = Vec::new(); let mut containers = Vec::new();
for container_image in images { for container_image in images {
let container = match &container_image { let container =
SlimImage::Debian { image, deps, .. } => { match &container_image {
let target = BuildTarget::from_target(&container_image); SlimImage::Debian { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self let build_container = self
.build( .build(
source_path.clone(), source_path.clone(),
&rust_version, &rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_env_variable("SQLX_OFFLINE", "true")
.with_exec(vec!["cargo", "clean"])
.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), BuildTarget::from_target(&container_image),
BuildProfile::Release, deps.iter()
crates, .map(|d| d.as_str())
extra_deps, .collect::<Vec<&str>>()
) .as_slice(),
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name, bin_name,
]) )
.file(format!( .await?
"target/{}/release/{}", }
target.to_string(), SlimImage::Alpine { image, deps, .. } => {
bin_name let target = BuildTarget::from_target(&container_image);
));
self.build_debian_image( let build_container = self
bin, .build(
image, source_path.clone(),
BuildTarget::from_target(&container_image), &rust_version,
deps.iter() BuildTarget::from_target(&container_image),
.map(|d| d.as_str()) BuildProfile::Release,
.collect::<Vec<&str>>() crates,
.as_slice(), extra_deps,
bin_name, )
) .await?;
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self let bin = build_container
.build( .with_exec(vec![
source_path.clone(), "cargo",
&rust_version, "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), BuildTarget::from_target(&container_image),
BuildProfile::Release, deps.iter()
crates, .map(|d| d.as_str())
extra_deps, .collect::<Vec<&str>>()
) .as_slice(),
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name, bin_name,
]) )
.file(format!( .await?
"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); containers.push(container);
} }
@@ -210,10 +208,10 @@ impl RustBuild {
.with_exec(packages); .with_exec(packages);
let final_image = base_debian let final_image = base_debian
.with_file(format!("/usr/local/bin/{}", bin_name), bin.id().await?) .with_file(format!("/usr/local/bin/{}", bin_name), bin)
.with_exec(vec![bin_name, "--help"]); .with_exec(vec![bin_name, "--help"]);
final_image.exit_code().await?; final_image.sync().await?;
Ok(final_image) Ok(final_image)
} }
@@ -238,8 +236,7 @@ impl RustBuild {
packages.extend_from_slice(production_deps); packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages); let base_debian = base_debian.with_exec(packages);
let final_image = let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin.id().await?);
Ok(final_image) Ok(final_image)
} }
@@ -252,7 +249,7 @@ pub enum RustVersion {
impl AsRef<RustVersion> for RustVersion { impl AsRef<RustVersion> for RustVersion {
fn as_ref(&self) -> &RustVersion { fn as_ref(&self) -> &RustVersion {
&self self
} }
} }
@@ -288,7 +285,7 @@ impl BuildTarget {
} }
} }
fn into_platform(&self) -> dagger_sdk::Platform { pub fn into_platform(&self) -> dagger_sdk::Platform {
let platform = match self { let platform = match self {
BuildTarget::LinuxAmd64 => "linux/amd64", BuildTarget::LinuxAmd64 => "linux/amd64",
BuildTarget::LinuxArm64 => "linux/arm64", BuildTarget::LinuxArm64 => "linux/arm64",
@@ -304,7 +301,7 @@ impl BuildTarget {
impl AsRef<BuildTarget> for BuildTarget { impl AsRef<BuildTarget> for BuildTarget {
fn as_ref(&self) -> &BuildTarget { fn as_ref(&self) -> &BuildTarget {
&self self
} }
} }
@@ -330,7 +327,7 @@ pub enum BuildProfile {
impl AsRef<BuildProfile> for BuildProfile { impl AsRef<BuildProfile> for BuildProfile {
fn as_ref(&self) -> &BuildProfile { fn as_ref(&self) -> &BuildProfile {
&self self
} }
} }

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 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

@@ -1,3 +1,6 @@
pub mod build; pub mod build;
pub mod htmx;
pub mod leptos;
pub mod publish;
pub mod source; pub mod source;
pub mod test; 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

@@ -1,18 +1,17 @@
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use eyre::Context; use eyre::Context;
pub struct RustSource { pub struct RustSource {
client: Arc<dagger_sdk::Query>, client: dagger_sdk::Query,
exclude: Vec<String>, exclude: Vec<String>,
} }
impl RustSource { impl RustSource {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self { pub fn new(client: dagger_sdk::Query) -> Self {
Self { Self {
client, client,
exclude: vec!["node_modules/", ".git/", "target/", ".cuddle/"] exclude: vec!["node_modules/", ".git/", "target/", ".cuddle/"]
@@ -63,7 +62,7 @@ impl RustSource {
let src = self.get_src(source.clone()).await?; let src = self.get_src(source.clone()).await?;
let rust_src = self.get_rust_dep_src(source).await?; let rust_src = self.get_rust_dep_src(source).await?;
let rust_src = rust_src.with_directory(".", skeleton_files.id().await?); let rust_src = rust_src.with_directory(".", skeleton_files);
Ok((src, rust_src)) Ok((src, rust_src))
} }
@@ -90,10 +89,14 @@ impl RustSource {
) -> eyre::Result<dagger_sdk::Directory> { ) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from(".")); 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( let directory = self.client.host().directory_opts(
source.display().to_string(), source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default() dagger_sdk::HostDirectoryOptsBuilder::default()
.include(vec!["**/Cargo.toml", "**/Cargo.lock"]) //.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.exclude(excludes.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?, .build()?,
); );
@@ -119,7 +122,7 @@ impl RustSource {
let incremental_dir = self.client.directory().with_directory_opts( let incremental_dir = self.client.directory().with_directory_opts(
".", ".",
container.directory("target").id().await?, container.directory("target"),
dagger_sdk::DirectoryWithDirectoryOpts { dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude), exclude: Some(exclude),
include: None, include: None,

View File

@@ -1,14 +1,14 @@
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf};
use crate::{build::RustVersion, source::RustSource}; use crate::{build::RustVersion, source::RustSource};
pub struct RustTest { pub struct RustTest {
client: Arc<dagger_sdk::Query>, client: dagger_sdk::Query,
registry: Option<String>, registry: Option<String>,
} }
impl RustTest { impl RustTest {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self { pub fn new(client: dagger_sdk::Query) -> Self {
Self { Self {
client, client,
registry: None, registry: None,
@@ -45,9 +45,9 @@ impl RustTest {
let build_options = vec!["cargo", "build", "--workspace"]; let build_options = vec!["cargo", "build", "--workspace"];
let rust_prebuild = rust_build_image let rust_prebuild = rust_build_image
.with_workdir("/mnt/src") .with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src.id().await?) .with_directory("/mnt/src", dep_src)
.with_exec(build_options) .with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache.id().await?); .with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec()) .get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
@@ -57,10 +57,10 @@ impl RustTest {
.with_workdir("/mnt/src") .with_workdir("/mnt/src")
.with_directory( .with_directory(
"/usr/local/cargo", "/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo").id().await?, rust_prebuild.directory("/usr/local/cargo"),
) )
.with_directory("/mnt/src/target", incremental_dir.id().await?) .with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src.id().await?); .with_directory("/mnt/src/", src);
let test = rust_with_src.with_exec(vec!["cargo", "test"]); let test = rust_with_src.with_exec(vec!["cargo", "test"]);
@@ -68,9 +68,7 @@ impl RustTest {
let stderr = test.stderr().await?; let stderr = test.stderr().await?;
println!("stdout: {}, stderr: {}", stdout, stderr); println!("stdout: {}, stderr: {}", stdout, stderr);
if 0 != test.exit_code().await? { test.sync().await?;
eyre::bail!("failed rust:test");
}
Ok(()) Ok(())
} }

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::{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

@@ -22,7 +22,7 @@ pub async fn main() -> eyre::Result<()> {
.await?; .await?;
for container in containers { for container in containers {
container.exit_code().await?; container.sync().await?;
} }
Ok(()) Ok(())

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"