Skip to content

Commit ecd9e1a

Browse files
committed
wip: try smart-default
1 parent 5bb0f4e commit ecd9e1a

File tree

12 files changed

+316
-65
lines changed

12 files changed

+316
-65
lines changed

CONTRIBUTING.md

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
11
# Contributing
22

3-
This guide contains helpful information for first time contributors.
3+
This guide contains some information for first time contributors.
44

5-
## Crate architecture
5+
## Architecture
66

77
There are currently four crates to know about:
88

99
- `goldboot`
10-
- The primary CLI application for building goldboot images
10+
- The primary CLI application for building images
1111
- `goldboot-image`
1212
- Implements the goldboot image format
1313
- `goldboot-macros`
14-
- Procedural macros
14+
- Procedural macros to simplify implementation of the `goldboot` crate
1515
- `goldboot-registry`
1616
- Web service that hosts goldboot images
1717

1818
## The metallurgy metaphor
1919

20-
Although end-users could probably ignore it, the internals of `goldboot` use
21-
vocabulary taken from the field of metallurgy.
20+
Although end-users could mostly ignore it, the internals of `goldboot` use
21+
vocabulary appropriated from the field of metallurgy.
2222

23-
#### Foundry
23+
### Element
2424

25-
An image foundry is a configuration object that knows how to build goldboot
26-
images.
25+
An image element takes an image source and refines it according to built-in
26+
rules.
2727

28-
#### OS
28+
For example, the `ArchLinux` element knows how to take Arch Linux install media
29+
(in the form of an ISO) and install it in an automated manner.
2930

30-
An image mold takes an image source and refines it according to built-in rules.
31-
For example, the `ArchLinux` mold knows how to take Arch Linux install media (in
32-
the form of an ISO) and install it in an automated manner.
33-
34-
#### Building
31+
### Building
3532

3633
Building is the process that takes image sources and produces a final goldboot
3734
image containing all customizations.
@@ -41,21 +38,27 @@ running one or more image molds against it (via SSH or VNC). Once the virtual
4138
machine is fully provisioned, its shutdown and the underlying storage (.qcow2)
4239
is converted into a final goldboot image (.gb).
4340

44-
#### Alloys
41+
### Alloys
4542

46-
An alloy is a multi-boot image.
43+
An alloy is a multi-boot image that consists of more than one element.
4744

48-
#### Fabricators
45+
### Fabricators
4946

5047
Operates on images at the end of the casting process. For example, the shell
5148
fabricator runs shell commands on the image which can be useful in many cases.
5249

53-
## Adding new operating systems
50+
### Casting
51+
52+
If we followed the metaphor strictly, then _casting_ would be a synonym for
53+
_building_, but we decided instead to make it be the process of applying an
54+
image to a device.
55+
56+
## Supporting new operating systems
5457

5558
If `goldboot` doesn't already support your operating system, it should be
5659
possible to add it relatively easily.
5760

58-
Start by finding an OS similar to yours in the `goldboot::foundry::os` module.
61+
Start by finding an OS similar to yours in the `goldboot::builder::os` module.
5962

6063
TODO
6164

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

goldboot-macros/src/lib.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ pub fn prompt(input: TokenStream) -> TokenStream {
99
impl_prompt(&ast)
1010
}
1111

12+
/// Automatically implement the "size()" method from BuildImage trait.
13+
/// This assumes the struct has a field named "size" of type Size.
14+
#[proc_macro_derive(BuildImageSize)]
15+
pub fn build_image_size(input: TokenStream) -> TokenStream {
16+
let ast = syn::parse(input).unwrap();
17+
impl_build_image_size(&ast)
18+
}
19+
1220
fn impl_prompt(ast: &syn::DeriveInput) -> TokenStream {
1321
let name = &ast.ident;
1422

@@ -77,4 +85,33 @@ fn is_option_type(ty: &syn::Type) -> bool {
7785
false
7886
}
7987

88+
fn impl_build_image_size(ast: &syn::DeriveInput) -> TokenStream {
89+
let name = &ast.ident;
90+
91+
// Verify that the struct has a field named "size"
92+
let has_size_field = match &ast.data {
93+
syn::Data::Struct(data) => match &data.fields {
94+
syn::Fields::Named(fields) => fields
95+
.named
96+
.iter()
97+
.any(|f| f.ident.as_ref().map(|i| i == "size").unwrap_or(false)),
98+
_ => false,
99+
},
100+
_ => false,
101+
};
102+
103+
if !has_size_field {
104+
panic!("BuildImageSize derive requires a field named 'size'");
105+
}
106+
107+
let syntax = quote! {
108+
impl BuildImage for #name {
109+
fn size(&self) -> &crate::builder::options::size::Size {
110+
&self.size
111+
}
112+
}
113+
};
114+
syntax.into()
115+
}
116+
80117
// TODO add pyclass

goldboot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ rustls = { version = "0.23.23" }
4242
serde_json = { version = "1.0.108", optional = true }
4343
serde = { workspace = true }
4444
serde_win_unattend = { version = "0.3.3", optional = true }
45+
smart-default = "0.7.1"
4546
serde_yaml = { version = "0.9.27", optional = true }
4647
sha1 = "0.10.6"
4748
sha2 = { workspace = true }

goldboot/src/builder/mod.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,13 @@ impl Builder {
7777
}
7878

7979
/// The system architecture
80-
pub fn arch(&self) -> Option<ImageArch> {
81-
todo!()
80+
pub fn arch(&self) -> Result<ImageArch> {
81+
match self.elements.first() {
82+
Some(element) => {
83+
todo!()
84+
}
85+
None => bail!("No elements in builder"),
86+
}
8287
}
8388

8489
/// Run the image build process according to the given command line.
@@ -137,11 +142,7 @@ impl Builder {
137142
.to_string();
138143

139144
#[cfg(feature = "include_ovmf")]
140-
crate::builder::ovmf::write(
141-
self.arch().ok_or_else(|| anyhow!("No elements"))?,
142-
&path,
143-
)
144-
.unwrap();
145+
crate::builder::ovmf::write(self.arch()?, &path).unwrap();
145146
self.ovmf_path = PathBuf::from(path);
146147
}
147148
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use crate::{builder::Builder, cli::prompt::Prompt};
2+
use anyhow::Result;
3+
use goldboot_image::ImageArch;
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Clone, Serialize, Deserialize, Debug)]
7+
pub struct Arch(pub ImageArch);
8+
9+
impl Prompt for Arch {
10+
fn prompt(&mut self, builder: &Builder) -> Result<()> {
11+
todo!()
12+
}
13+
}

goldboot/src/builder/options/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod arch;
12
pub mod hostname;
23
pub mod iso;
34
pub mod luks;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,177 @@
1+
use crate::{builder::Builder, cli::prompt::Prompt};
2+
use anyhow::Result;
3+
use byte_unit::Byte;
4+
use serde::{Deserialize, Serialize};
5+
use validator::Validate;
6+
17
/// Absolute size of an image element.
8+
#[derive(Clone, Serialize, Deserialize, Debug)]
29
pub struct Size(String);
10+
11+
impl Default for Size {
12+
fn default() -> Self {
13+
Self("16G".to_string())
14+
}
15+
}
16+
17+
impl Prompt for Size {
18+
fn prompt(&mut self, builder: &Builder) -> Result<()> {
19+
todo!()
20+
}
21+
}
22+
23+
impl Validate for Size {
24+
fn validate(&self) -> std::result::Result<(), validator::ValidationErrors> {
25+
// Try to parse the size string using byte-unit
26+
match self.0.parse::<Byte>() {
27+
Ok(byte) => {
28+
// Ensure the size is greater than zero
29+
if byte.as_u64() == 0 {
30+
let mut errors = validator::ValidationErrors::new();
31+
errors.add(
32+
"size",
33+
validator::ValidationError::new("Size must be greater than zero"),
34+
);
35+
return Err(errors);
36+
}
37+
Ok(())
38+
}
39+
Err(_) => {
40+
let mut errors = validator::ValidationErrors::new();
41+
errors.add(
42+
"size",
43+
validator::ValidationError::new("Invalid size format. Expected format: number followed by unit (e.g., '16G', '512M', '1T')"),
44+
);
45+
Err(errors)
46+
}
47+
}
48+
}
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
55+
#[test]
56+
fn test_valid_sizes() {
57+
// Test various valid size formats
58+
let sizes = vec![
59+
"16G",
60+
"16GB",
61+
"512M",
62+
"512MB",
63+
"1T",
64+
"1TB",
65+
"2048K",
66+
"2048KB",
67+
"1024",
68+
"1 GB",
69+
"1.5 GB",
70+
"100 MiB",
71+
"16GiB",
72+
];
73+
74+
for size_str in sizes {
75+
let size = Size(size_str.to_string());
76+
assert!(
77+
size.validate().is_ok(),
78+
"Expected '{}' to be valid",
79+
size_str
80+
);
81+
}
82+
}
83+
84+
#[test]
85+
fn test_invalid_sizes() {
86+
// Test various invalid size formats
87+
let invalid_sizes = vec![
88+
"", // Empty string
89+
"abc", // No numbers
90+
"G16", // Unit before number
91+
"16X", // Invalid unit
92+
"hello world", // Completely invalid
93+
];
94+
95+
for size_str in invalid_sizes {
96+
let size = Size(size_str.to_string());
97+
assert!(
98+
size.validate().is_err(),
99+
"Expected '{}' to be invalid",
100+
size_str
101+
);
102+
}
103+
}
104+
105+
#[test]
106+
fn test_zero_size() {
107+
// Test that zero size is rejected
108+
let size = Size("0".to_string());
109+
assert!(
110+
size.validate().is_err(),
111+
"Expected zero size to be invalid"
112+
);
113+
114+
let size = Size("0GB".to_string());
115+
assert!(
116+
size.validate().is_err(),
117+
"Expected zero size to be invalid"
118+
);
119+
}
120+
121+
#[test]
122+
fn test_default_size() {
123+
// Test that the default size is valid
124+
let size = Size::default();
125+
assert!(
126+
size.validate().is_ok(),
127+
"Expected default size '{}' to be valid",
128+
size.0
129+
);
130+
}
131+
132+
#[test]
133+
fn test_large_sizes() {
134+
// Test large sizes
135+
let sizes = vec![
136+
"1000T",
137+
"1000TB",
138+
"1PB",
139+
"100000GB",
140+
];
141+
142+
for size_str in sizes {
143+
let size = Size(size_str.to_string());
144+
assert!(
145+
size.validate().is_ok(),
146+
"Expected large size '{}' to be valid",
147+
size_str
148+
);
149+
}
150+
}
151+
152+
#[test]
153+
fn test_bytes_only() {
154+
// Test sizes specified in bytes only
155+
let size = Size("1073741824".to_string()); // 1GB in bytes
156+
assert!(
157+
size.validate().is_ok(),
158+
"Expected bytes-only size to be valid"
159+
);
160+
}
161+
162+
#[test]
163+
fn test_binary_vs_decimal_units() {
164+
// Test both binary (GiB) and decimal (GB) units
165+
let size_binary = Size("16GiB".to_string());
166+
assert!(
167+
size_binary.validate().is_ok(),
168+
"Expected binary unit GiB to be valid"
169+
);
170+
171+
let size_decimal = Size("16GB".to_string());
172+
assert!(
173+
size_decimal.validate().is_ok(),
174+
"Expected decimal unit GB to be valid"
175+
);
176+
}
177+
}

0 commit comments

Comments
 (0)