Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bc1d5303 | ||
|
|
d342370b89 |
No files matched your search
Generated
+4
@@ -164,6 +164,10 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scaffolding"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ members = [
|
|||||||
"src/machoke",
|
"src/machoke",
|
||||||
"src/monorepo",
|
"src/monorepo",
|
||||||
"src/status",
|
"src/status",
|
||||||
|
"tools/scaffolding",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ Confirm you have connectivity (`ping -c1 github.com`) before continuing.
|
|||||||
Clone the monorepo onto the live ISO and work from that local checkout:
|
Clone the monorepo onto the live ISO and work from that local checkout:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ git clone ssh://gitea@git.alexion.dev:2022/alexion/dev.git
|
$ git clone ssh://gitea@git.alexion.dev/alexion/dev.git
|
||||||
$ cd dev
|
$ cd dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
registry,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.gitea;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.gitea.dataPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/gitea";
|
||||||
|
description = "Host path mounted as Gitea's state directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.dataPath != null;
|
||||||
|
message = "guests.gitea.dataPath must point at the host directory that stores Gitea state.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
registry.homepage.${config.networking.hostName}.gitea = {
|
||||||
|
tab = "Apps";
|
||||||
|
section = "Other";
|
||||||
|
name = "Gitea";
|
||||||
|
description = "Git forge";
|
||||||
|
icon = "gitea.png";
|
||||||
|
};
|
||||||
|
|
||||||
|
guests.gitea = {
|
||||||
|
interior.modules.services.gitea = {
|
||||||
|
enable = true;
|
||||||
|
reverseProxyAddress = registry.address.${config.networking.hostName}."reverse-proxy";
|
||||||
|
};
|
||||||
|
|
||||||
|
mounts."/var/lib/gitea" = {
|
||||||
|
directory = cfg.dataPath;
|
||||||
|
owner = config.registry."stable-uid".${config.networking.hostName}.gitea;
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
registry,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
giteaSshBackend = "10.23.20.45:2022";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
config = lib.mkIf config.guests.reverse-proxy.enable {
|
config = lib.mkIf config.guests.reverse-proxy.enable {
|
||||||
guests.reverse-proxy = {
|
guests.reverse-proxy = {
|
||||||
@@ -13,11 +11,6 @@ in
|
|||||||
modules.services.reverse-proxy.enable = true;
|
modules.services.reverse-proxy.enable = true;
|
||||||
|
|
||||||
services.nginx.virtualHosts = {
|
services.nginx.virtualHosts = {
|
||||||
"git.alexion.dev" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/".proxyPass = "http://10.23.20.32";
|
|
||||||
};
|
|
||||||
"photos.alexion.dev" = {
|
"photos.alexion.dev" = {
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
@@ -45,12 +38,12 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 2022 ];
|
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||||
|
|
||||||
services.nginx.streamConfig = ''
|
services.nginx.streamConfig = ''
|
||||||
server {
|
server {
|
||||||
listen 2022;
|
listen 22;
|
||||||
proxy_pass ${giteaSshBackend};
|
proxy_pass ${registry.address.${config.networking.hostName}.gitea}:2222;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -96,6 +96,21 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
guests.gitea = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
address = "10.23.20.206";
|
||||||
|
dataPath = "/pikachu/data/gitea";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
route = {
|
||||||
|
domain = "git.alexion.dev";
|
||||||
|
protocol = "https";
|
||||||
|
port = 3000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
guests.romm = {
|
guests.romm = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
|
|||||||
@@ -33,11 +33,14 @@ let
|
|||||||
});
|
});
|
||||||
|
|
||||||
extraTargets = {
|
extraTargets = {
|
||||||
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
|
||||||
gitea = {
|
gitea = {
|
||||||
hostName = "git.alexion.dev";
|
hostName = "git.alexion.dev";
|
||||||
port = 2022;
|
|
||||||
user = "gitea";
|
user = "gitea";
|
||||||
|
hostKeys = [
|
||||||
|
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFcUl17AcZz1wS8ek2yS/bu5NRTX4ViYTuFOSnp8leHmirP3Zk5QlC33hodQp09J6EA6Vl8zUowy4qLud7BMurQ="
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICkK2W2Ev+9RXa2PNXiNPPCPF5ln2+M2jHiGoZ4cPYxX"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDZSeU1iRHoaIXgbYe0kAiOAASw9LrQhu54RNCoTQ2O1rbqgnnKO2qbYJvfmUWL7jqEkEotgUBy4Xqxgfl7rXGQ9viWZe+i2tKwJdGsabNLJ+QTDCNyGCd22s1Hhl7n68JvuLTENXl4WpH56MB3yjFmK9onLHHcLG+rWiR0I6Yn264LfNVX0RuZK7TlG0tUKJR5bQBXGT6PxAE1IceYRgbgM0BYq4nakNg/s4ZSpmJFrrGfCHNsnHiEmFAXRFrGwf+0ef2STDukM/ADLziPXu0Ge7k72/UPvtEdcpgvzIDMJ2EznKTnxIoZnFkrDqhGOo8DnXK+cFVKAkkC6NgWQpO6fNnY+MEDgSb0fr/rGcwzUa1p+fON5/zEYxsAtKyAeP4D7OEAo65lsSKqi1tbt521miTobp0NOT8p4tGgHTwmshbqlmUZz0DVexN6uc7yzoHu4V/oYomEWP/j/E2uuvLi57AaJbM3Z/oAHUtrHFK9A9FFggyzpETZE7qWA9BhO/pJQUsjdWRm0pUHsPlf9Uqqit2BKVERiMSlHsBKch4tIk4ryZBazWunDD+QFcllKG74nMVISZ7i8q9IfNy+PWQuJIEm5XrT9W5pnkBIWrePlcAcPo080ALigmyhH+WOPiyO+dfueeq6PVdu2Voc+6FvtylRcn4u8O+E5Wq5amvQAQ=="
|
||||||
|
];
|
||||||
aliases = [
|
aliases = [
|
||||||
"gitea"
|
"gitea"
|
||||||
"git.alexion.dev"
|
"git.alexion.dev"
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.services.gitea;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.services.gitea = {
|
||||||
|
enable = lib.mkEnableOption "Gitea";
|
||||||
|
|
||||||
|
reverseProxyAddress = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "IP address of the reverse proxy trusted to forward client headers.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
{
|
||||||
|
registry."stable-uid".${config.networking.hostName}.gitea = 20007;
|
||||||
|
}
|
||||||
|
|
||||||
|
(lib.mkIf cfg.enable {
|
||||||
|
users.users.gitea = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = config.registry."stable-uid".${config.networking.hostName}.gitea;
|
||||||
|
group = "storage";
|
||||||
|
home = "/var/lib/gitea";
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
2222
|
||||||
|
3000
|
||||||
|
];
|
||||||
|
|
||||||
|
services.gitea = {
|
||||||
|
enable = true;
|
||||||
|
user = "gitea";
|
||||||
|
group = "storage";
|
||||||
|
stateDir = "/var/lib/gitea";
|
||||||
|
repositoryRoot = "/var/lib/gitea/repositories";
|
||||||
|
lfs = {
|
||||||
|
enable = true;
|
||||||
|
contentDir = "/var/lib/gitea/data/lfs";
|
||||||
|
};
|
||||||
|
database.type = "sqlite3";
|
||||||
|
settings = {
|
||||||
|
server = {
|
||||||
|
HTTP_ADDR = "0.0.0.0";
|
||||||
|
HTTP_PORT = 3000;
|
||||||
|
ROOT_URL = "https://git.alexion.dev/";
|
||||||
|
SSH_DOMAIN = "git.alexion.dev";
|
||||||
|
SSH_PORT = 22;
|
||||||
|
START_SSH_SERVER = true;
|
||||||
|
SSH_LISTEN_HOST = "0.0.0.0";
|
||||||
|
SSH_LISTEN_PORT = 2222;
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.reverseProxyAddress != null) {
|
||||||
|
REVERSE_PROXY_TRUSTED_PROXIES = "${cfg.reverseProxyAddress}/32";
|
||||||
|
};
|
||||||
|
service.DISABLE_REGISTRATION = true;
|
||||||
|
session.COOKIE_SECURE = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||||
|
|
||||||
|
rust_library(
|
||||||
|
name = "rust",
|
||||||
|
crate_name = "rust",
|
||||||
|
srcs = [
|
||||||
|
"rust.rs",
|
||||||
|
"rust_test.rs",
|
||||||
|
],
|
||||||
|
compile_data = glob(["templates/*"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "rust_test",
|
||||||
|
crate = ":rust",
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_binary(
|
||||||
|
name = "rust_cli",
|
||||||
|
crate_name = "rust_cli",
|
||||||
|
srcs = [
|
||||||
|
"rust_main.rs",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
":rust",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "scaffolding"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rust"
|
||||||
|
path = "rust.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rust_cli"
|
||||||
|
path = "rust_main.rs"
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const BUILD_TEMPLATE: &str = include_str!("templates/BUILD.template");
|
||||||
|
const CARGO_TEMPLATE: &str = include_str!("templates/Cargo.toml.template");
|
||||||
|
const LIBRARY_TEMPLATE: &str = include_str!("templates/library.rs.template");
|
||||||
|
const BINARY_TEMPLATE: &str = include_str!("templates/binary.rs.template");
|
||||||
|
const TEST_TEMPLATE: &str = include_str!("templates/test.rs.template");
|
||||||
|
const CRATE_NAME_PLACEHOLDER: &str = "{CRATE_NAME}";
|
||||||
|
|
||||||
|
/// Creates a Rust package at `destination`.
|
||||||
|
pub fn run(destination: &Path) -> io::Result<PathBuf> {
|
||||||
|
scaffold(&workspace_root()?, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scaffold(workspace_root: &Path, destination: &Path) -> io::Result<PathBuf> {
|
||||||
|
let workspace_root = fs::canonicalize(workspace_root)?;
|
||||||
|
let destination = resolve_destination(&workspace_root, destination)?;
|
||||||
|
let crate_name = crate_name(&destination)?;
|
||||||
|
let workspace_member = workspace_member(&workspace_root, &destination)?;
|
||||||
|
let files = render_files(&crate_name);
|
||||||
|
let cargo_path = workspace_root.join("Cargo.toml");
|
||||||
|
let cargo_manifest = fs::read_to_string(&cargo_path)?;
|
||||||
|
let cargo_manifest = update_workspace_members(&cargo_manifest, &workspace_member)?;
|
||||||
|
|
||||||
|
fs::create_dir(&destination)?;
|
||||||
|
for file in files {
|
||||||
|
fs::write(destination.join(file.name), file.contents)?;
|
||||||
|
}
|
||||||
|
fs::write(cargo_path, cargo_manifest)?;
|
||||||
|
|
||||||
|
Ok(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> io::Result<PathBuf> {
|
||||||
|
match env::var_os("BUILD_WORKSPACE_DIRECTORY") {
|
||||||
|
Some(path) => Ok(PathBuf::from(path)),
|
||||||
|
None => env::current_dir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_destination(workspace_root: &Path, destination: &Path) -> io::Result<PathBuf> {
|
||||||
|
let destination = if destination.is_absolute() {
|
||||||
|
destination.to_path_buf()
|
||||||
|
} else {
|
||||||
|
workspace_root.join(destination)
|
||||||
|
};
|
||||||
|
let parent = destination.parent().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination must have a parent directory",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let parent = fs::canonicalize(parent)?;
|
||||||
|
|
||||||
|
if !parent.starts_with(workspace_root) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!(
|
||||||
|
"the destination is outside the workspace: {}",
|
||||||
|
destination.display()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let destination = parent.join(destination.file_name().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination must name a directory",
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
if destination.try_exists()? {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("the destination already exists: {}", destination.display()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crate_name(destination: &Path) -> io::Result<String> {
|
||||||
|
let crate_name = destination
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination name must be valid UTF-8",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if crate_name.is_empty()
|
||||||
|
|| !crate_name.bytes().all(|character| {
|
||||||
|
character.is_ascii_lowercase() || character.is_ascii_digit() || character == b'_'
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid crate name: {crate_name}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(crate_name.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_member(workspace_root: &Path, destination: &Path) -> io::Result<String> {
|
||||||
|
let member = destination.strip_prefix(workspace_root).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination is outside the workspace",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let member = member.to_str().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"the destination must be valid UTF-8",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(member.replace(std::path::MAIN_SEPARATOR, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_files(crate_name: &str) -> Vec<RenderedFile> {
|
||||||
|
vec![
|
||||||
|
render_file("BUILD", BUILD_TEMPLATE, crate_name),
|
||||||
|
render_file("Cargo.toml", CARGO_TEMPLATE, crate_name),
|
||||||
|
render_file(&format!("{crate_name}.rs"), LIBRARY_TEMPLATE, crate_name),
|
||||||
|
render_file(
|
||||||
|
&format!("{crate_name}_main.rs"),
|
||||||
|
BINARY_TEMPLATE,
|
||||||
|
crate_name,
|
||||||
|
),
|
||||||
|
render_file(&format!("{crate_name}_test.rs"), TEST_TEMPLATE, crate_name),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_file(name: &str, template: &str, crate_name: &str) -> RenderedFile {
|
||||||
|
RenderedFile {
|
||||||
|
name: name.to_owned(),
|
||||||
|
contents: template.replace(CRATE_NAME_PLACEHOLDER, crate_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_workspace_members(manifest: &str, member: &str) -> io::Result<String> {
|
||||||
|
let workspace_start = manifest.find("[workspace]").ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"the root Cargo.toml has no [workspace] section",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let members_start = manifest[workspace_start..]
|
||||||
|
.find("members = [")
|
||||||
|
.map(|offset| workspace_start + offset)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"the root Cargo.toml has no workspace members array",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let members_end = manifest[members_start..]
|
||||||
|
.find(']')
|
||||||
|
.map(|offset| members_start + offset)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"the root Cargo.toml has an unclosed workspace members array",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if manifest[members_start..members_end].contains(&format!("\"{member}\"")) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("the workspace already has member: {member}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut manifest = manifest.to_owned();
|
||||||
|
manifest.insert_str(members_end, &format!(" \"{member}\",\n"));
|
||||||
|
Ok(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RenderedFile {
|
||||||
|
name: String,
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "rust_test.rs"]
|
||||||
|
mod rust_test;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let mut arguments = env::args_os();
|
||||||
|
let program = arguments.next().unwrap_or_else(|| "rust_cli".into());
|
||||||
|
let Some(destination) = arguments.next() else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("usage: {} <destination>", PathBuf::from(program).display()),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if destination == "--help" {
|
||||||
|
println!("usage: {} <destination>", PathBuf::from(program).display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if arguments.next().is_some() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"expected exactly one destination",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let destination = rust::run(&PathBuf::from(destination))?;
|
||||||
|
println!("created {}", destination.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use super::{crate_name, render_files, scaffold, update_workspace_members};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_valid_crate_names() {
|
||||||
|
assert_eq!(crate_name(Path::new("foo_2")).unwrap(), "foo_2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_crate_names() {
|
||||||
|
assert!(crate_name(Path::new("Foo")).is_err());
|
||||||
|
assert!(crate_name(Path::new("foo-bar")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_crate_name_into_every_file() {
|
||||||
|
for file in render_files("example") {
|
||||||
|
assert!(!file.contents.contains("{CRATE_NAME}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adds_workspace_member() {
|
||||||
|
let manifest = "[workspace]\nmembers = [\n \"src/status\",\n]\n";
|
||||||
|
let updated = update_workspace_members(manifest, "src/example").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
updated,
|
||||||
|
"[workspace]\nmembers = [\n \"src/status\",\n \"src/example\",\n]\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_a_rust_package() {
|
||||||
|
let workspace = std::env::temp_dir().join(format!(
|
||||||
|
"scaffolding-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos(),
|
||||||
|
));
|
||||||
|
fs::create_dir_all(workspace.join("src")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
workspace.join("Cargo.toml"),
|
||||||
|
"[workspace]\nmembers = [\n]\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let destination = scaffold(&workspace, &workspace.join("src/example")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
destination,
|
||||||
|
fs::canonicalize(workspace.join("src/example")).unwrap()
|
||||||
|
);
|
||||||
|
assert!(destination.join("BUILD").exists());
|
||||||
|
assert!(destination.join("Cargo.toml").exists());
|
||||||
|
assert!(destination.join("example.rs").exists());
|
||||||
|
assert!(destination.join("example_main.rs").exists());
|
||||||
|
assert!(destination.join("example_test.rs").exists());
|
||||||
|
assert!(
|
||||||
|
fs::read_to_string(workspace.join("Cargo.toml"))
|
||||||
|
.unwrap()
|
||||||
|
.contains(" \"src/example\",")
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(workspace).unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||||
|
|
||||||
|
rust_library(
|
||||||
|
name = "{CRATE_NAME}",
|
||||||
|
crate_name = "{CRATE_NAME}",
|
||||||
|
srcs = [
|
||||||
|
"{CRATE_NAME}.rs",
|
||||||
|
"{CRATE_NAME}_test.rs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_test(
|
||||||
|
name = "{CRATE_NAME}_test",
|
||||||
|
crate = ":{CRATE_NAME}",
|
||||||
|
)
|
||||||
|
|
||||||
|
rust_binary(
|
||||||
|
name = "{CRATE_NAME}_cli",
|
||||||
|
crate_name = "{CRATE_NAME}_cli",
|
||||||
|
srcs = [
|
||||||
|
"{CRATE_NAME}_main.rs",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
":{CRATE_NAME}",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "{CRATE_NAME}"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "{CRATE_NAME}.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "{CRATE_NAME}_cli"
|
||||||
|
path = "{CRATE_NAME}_main.rs"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
{CRATE_NAME}::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
#[path = "{CRATE_NAME}_test.rs"]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub fn run() {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use super::run;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runs() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user