Add Forgejo Actions workflows for automated builds and artifact uploads
- Add comprehensive build-artifacts.yml workflow with Forgejo Package Registry upload - Add simple-build.yml workflow for basic artifact management - Update README.md with workflow documentation and setup instructions - Fix debian/rules to correctly create bootupctl symlink to /usr/libexec/bootupd - Improve error handling and validation throughout the codebase - Remove unused functions and imports - Update documentation to clarify bootupd is not a daemon - Fix binary layout to match RPM packaging pattern
This commit is contained in:
parent
aaf662d5b1
commit
95c23891b6
10 changed files with 790 additions and 145 deletions
|
|
@ -50,10 +50,20 @@ pub(crate) fn install(
|
|||
target_components: Option<&[String]>,
|
||||
auto_components: bool,
|
||||
) -> Result<()> {
|
||||
// Validate input parameters
|
||||
if source_root.is_empty() {
|
||||
anyhow::bail!("source_root cannot be empty");
|
||||
}
|
||||
if dest_root.is_empty() {
|
||||
anyhow::bail!("dest_root cannot be empty");
|
||||
}
|
||||
|
||||
// TODO: Change this to an Option<&str>; though this probably balloons into having
|
||||
// DeviceComponent and FileBasedComponent
|
||||
let device = device.unwrap_or("");
|
||||
let source_root = openat::Dir::open(source_root).context("Opening source root")?;
|
||||
let source_root = openat::Dir::open(source_root)
|
||||
.with_context(|| format!("Opening source root: {}", source_root))?;
|
||||
|
||||
SavedState::ensure_not_present(dest_root)
|
||||
.context("failed to install, invalid re-install attempted")?;
|
||||
|
||||
|
|
@ -62,15 +72,23 @@ pub(crate) fn install(
|
|||
println!("No components available for this platform.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let target_components = if let Some(target_components) = target_components {
|
||||
// Checked by CLI parser
|
||||
assert!(!auto_components);
|
||||
if target_components.is_empty() {
|
||||
anyhow::bail!("No target components specified");
|
||||
}
|
||||
|
||||
target_components
|
||||
.iter()
|
||||
.map(|name| {
|
||||
if name.is_empty() {
|
||||
anyhow::bail!("Component name cannot be empty");
|
||||
}
|
||||
all_components
|
||||
.get(name.as_str())
|
||||
.ok_or_else(|| anyhow!("Unknown component: {name}"))
|
||||
.ok_or_else(|| anyhow!("Unknown component: '{}'", name))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
} else {
|
||||
|
|
@ -81,6 +99,8 @@ pub(crate) fn install(
|
|||
anyhow::bail!("No components specified");
|
||||
}
|
||||
|
||||
log::info!("Installing {} components to {}", target_components.len(), dest_root);
|
||||
|
||||
let mut state = SavedState::default();
|
||||
let mut installed_efi_vendor = None;
|
||||
for &component in target_components.iter() {
|
||||
|
|
@ -434,7 +454,10 @@ pub(crate) fn print_status(status: &Status) -> Result<()> {
|
|||
}
|
||||
|
||||
if let Some(coreos_aleph) = coreos::get_aleph_version(Path::new("/"))? {
|
||||
println!("CoreOS aleph version: {}", coreos_aleph.version_info.version);
|
||||
println!(
|
||||
"CoreOS aleph version: {}",
|
||||
coreos_aleph.version_info.version
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
|
|
|
|||
|
|
@ -178,14 +178,18 @@ fn ensure_running_in_systemd() -> Result<()> {
|
|||
require_root_permission()?;
|
||||
let running_in_systemd = running_in_systemd();
|
||||
if !running_in_systemd {
|
||||
log::info!("Not running in systemd, re-executing via systemd-run");
|
||||
|
||||
// Clear any failure status that may have happened previously
|
||||
let _r = Command::new("systemctl")
|
||||
.arg("reset-failed")
|
||||
.arg("bootupd.service")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
.spawn()
|
||||
.and_then(|mut child| child.wait())
|
||||
.map_err(|e| log::warn!("Failed to reset failed status: {}", e));
|
||||
|
||||
let r = Command::new("systemd-run")
|
||||
.args(SYSTEMD_ARGS_BOOTUPD)
|
||||
.args(
|
||||
|
|
@ -196,7 +200,7 @@ fn ensure_running_in_systemd() -> Result<()> {
|
|||
.args(std::env::args())
|
||||
.exec();
|
||||
// If we got here, it's always an error
|
||||
return Err(r.into());
|
||||
return Err(anyhow::anyhow!("Failed to re-execute via systemd-run: {}", r));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,23 +34,9 @@ pub(crate) struct SystemVersionWithTimestamp {
|
|||
|
||||
/// Paths to version files for different systems
|
||||
const COREOS_ALEPH_PATH: &str = ".coreos-aleph-version.json";
|
||||
#[allow(dead_code)]
|
||||
const DEBIAN_VERSION_PATH: &str = ".debian-version.json";
|
||||
|
||||
/// Get version information for CoreOS or Debian systems
|
||||
pub(crate) fn get_system_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
|
||||
// Try CoreOS aleph version first
|
||||
if let Some(version) = get_coreos_version(root)? {
|
||||
return Ok(Some(version));
|
||||
}
|
||||
|
||||
// Try Debian version
|
||||
if let Some(version) = get_debian_version(root)? {
|
||||
return Ok(Some(version));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get CoreOS aleph version (original functionality)
|
||||
pub(crate) fn get_coreos_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
|
||||
let path = &root.join(COREOS_ALEPH_PATH);
|
||||
|
|
@ -61,10 +47,13 @@ pub(crate) fn get_coreos_version(root: &Path) -> Result<Option<SystemVersionWith
|
|||
let meta = statusf.metadata()?;
|
||||
let bufr = std::io::BufReader::new(statusf);
|
||||
let aleph: SystemVersion = serde_json::from_reader(bufr)?;
|
||||
|
||||
|
||||
// Use created time if available, otherwise fall back to modified time
|
||||
let ts = meta.created().unwrap_or_else(|_| meta.modified().unwrap()).into();
|
||||
|
||||
let ts = meta
|
||||
.created()
|
||||
.unwrap_or_else(|_| meta.modified().unwrap())
|
||||
.into();
|
||||
|
||||
Ok(Some(SystemVersionWithTimestamp {
|
||||
version_info: aleph,
|
||||
ts,
|
||||
|
|
@ -72,6 +61,7 @@ pub(crate) fn get_coreos_version(root: &Path) -> Result<Option<SystemVersionWith
|
|||
}
|
||||
|
||||
/// Get Debian version information
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_debian_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
|
||||
let path = &root.join(DEBIAN_VERSION_PATH);
|
||||
if !path.exists() {
|
||||
|
|
@ -81,10 +71,13 @@ pub(crate) fn get_debian_version(root: &Path) -> Result<Option<SystemVersionWith
|
|||
let meta = statusf.metadata()?;
|
||||
let bufr = std::io::BufReader::new(statusf);
|
||||
let deb_version: SystemVersion = serde_json::from_reader(bufr)?;
|
||||
|
||||
|
||||
// Use created time if available, otherwise fall back to modified time
|
||||
let ts = meta.created().unwrap_or_else(|_| meta.modified().unwrap()).into();
|
||||
|
||||
let ts = meta
|
||||
.created()
|
||||
.unwrap_or_else(|_| meta.modified().unwrap())
|
||||
.into();
|
||||
|
||||
Ok(Some(SystemVersionWithTimestamp {
|
||||
version_info: deb_version,
|
||||
ts,
|
||||
|
|
@ -130,7 +123,7 @@ mod test {
|
|||
println!("Creating file at: {:?}", file_path);
|
||||
std::fs::write(&file_path, V1_ALEPH_DATA)?;
|
||||
println!("File created successfully");
|
||||
|
||||
|
||||
let result = get_coreos_version(tempdir.path())?;
|
||||
println!("Result: {:?}", result);
|
||||
let Some(result) = result else {
|
||||
|
|
@ -175,13 +168,19 @@ mod test {
|
|||
#[test]
|
||||
fn test_parse_debian_version() -> Result<()> {
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
std::fs::write(tempdir.path().join(DEBIAN_VERSION_PATH), DEBIAN_VERSION_DATA)?;
|
||||
std::fs::write(
|
||||
tempdir.path().join(DEBIAN_VERSION_PATH),
|
||||
DEBIAN_VERSION_DATA,
|
||||
)?;
|
||||
let result = get_debian_version(tempdir.path())?;
|
||||
let Some(result) = result else {
|
||||
anyhow::bail!("Expected Some result");
|
||||
};
|
||||
assert_eq!(result.version_info.version, "12.1");
|
||||
assert_eq!(result.version_info.ref_name, Some("debian/bookworm/amd64".to_string()));
|
||||
assert_eq!(
|
||||
result.version_info.ref_name,
|
||||
Some("debian/bookworm/amd64".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::debug;
|
||||
|
||||
/// https://github.com/coreos/rpm-ostree/pull/969/commits/dc0e8db5bd92e1f478a0763d1a02b48e57022b59
|
||||
#[cfg(any(
|
||||
|
|
@ -18,49 +15,6 @@ use log::debug;
|
|||
))]
|
||||
pub(crate) const BOOT_PREFIX: &str = "usr/lib/ostree-boot";
|
||||
|
||||
/// Detect if this is a Debian-based system
|
||||
fn is_debian_system(sysroot: &Path) -> bool {
|
||||
// Check for Debian-specific files
|
||||
let debian_files = [
|
||||
"etc/debian_version",
|
||||
"var/lib/dpkg/status",
|
||||
];
|
||||
|
||||
for file in debian_files.iter() {
|
||||
if sysroot.join(file).exists() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check os-release content
|
||||
if let Ok(content) = std::fs::read_to_string(sysroot.join("etc/os-release")) {
|
||||
if content.contains("ID=debian") ||
|
||||
content.contains("ID=ubuntu") ||
|
||||
content.contains("ID=linuxmint") ||
|
||||
content.contains("ID=pop") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Create dpkg command for Debian systems
|
||||
pub(crate) fn dpkg_cmd<P: AsRef<Path>>(sysroot: P) -> Result<std::process::Command> {
|
||||
let c = std::process::Command::new("dpkg");
|
||||
let sysroot = sysroot.as_ref();
|
||||
|
||||
// Check if this is a Debian system
|
||||
if !is_debian_system(sysroot) {
|
||||
anyhow::bail!("Not a Debian system - dpkg command not available");
|
||||
}
|
||||
|
||||
// For OSTree systems, we might need to adjust paths
|
||||
// but dpkg typically works with the standard paths
|
||||
debug!("Using dpkg for Debian system");
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Get sysroot.bootloader in ostree repo config.
|
||||
pub(crate) fn get_ostree_bootloader() -> Result<Option<String>> {
|
||||
let mut cmd = std::process::Command::new("ostree");
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ use crate::model::*;
|
|||
|
||||
/// Parse the output of `dpkg -S` to extract package names
|
||||
fn parse_dpkg_s_output(output: &[u8]) -> Result<String> {
|
||||
let output_str = std::str::from_utf8(output)?;
|
||||
let output_str = std::str::from_utf8(output)
|
||||
.with_context(|| "dpkg output is not valid UTF-8")?;
|
||||
|
||||
// dpkg -S outputs "package: /path" format
|
||||
// Package names can contain colons (e.g., "grub-efi-amd64:amd64")
|
||||
// We need to find the colon that is followed by a space (start of file path)
|
||||
|
|
@ -26,22 +28,31 @@ fn parse_dpkg_s_output(output: &[u8]) -> Result<String> {
|
|||
}
|
||||
|
||||
if let Some(pos) = colon_pos {
|
||||
Ok(output_str[..pos].trim().to_string())
|
||||
let package_name = output_str[..pos].trim();
|
||||
if package_name.is_empty() {
|
||||
bail!("Package name is empty in dpkg output: {}", output_str);
|
||||
}
|
||||
Ok(package_name.to_string())
|
||||
} else {
|
||||
bail!("Invalid dpkg -S output format: {}", output_str)
|
||||
bail!("Invalid dpkg -S output format (no package:path separator): {}", output_str)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get package installation time from package.list file
|
||||
fn get_package_install_time(package: &str) -> Result<DateTime<Utc>> {
|
||||
if package.is_empty() {
|
||||
bail!("Package name cannot be empty");
|
||||
}
|
||||
|
||||
let list_path = format!("/var/lib/dpkg/info/{}.list", package);
|
||||
let metadata = std::fs::metadata(&list_path)
|
||||
.with_context(|| format!("Failed to get metadata for package {}", package))?;
|
||||
|
||||
.with_context(|| format!("Failed to get metadata for package '{}' at path '{}'", package, list_path))?;
|
||||
|
||||
// Use modification time as installation time
|
||||
let modified = metadata.modified()
|
||||
.with_context(|| format!("Failed to get modification time for package {}", package))?;
|
||||
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.with_context(|| format!("Failed to get modification time for package '{}'", package))?;
|
||||
|
||||
Ok(DateTime::from(modified))
|
||||
}
|
||||
|
||||
|
|
@ -50,19 +61,21 @@ fn dpkg_parse_metadata(packages: &BTreeSet<String>) -> Result<ContentMetadata> {
|
|||
if packages.is_empty() {
|
||||
bail!("Failed to find any Debian packages matching files in source efidir");
|
||||
}
|
||||
|
||||
|
||||
let mut timestamps = BTreeSet::new();
|
||||
|
||||
|
||||
// Get installation time for each package
|
||||
for package in packages {
|
||||
let timestamp = get_package_install_time(package)?;
|
||||
timestamps.insert(timestamp);
|
||||
}
|
||||
|
||||
|
||||
// Use the most recent timestamp
|
||||
let largest_timestamp = timestamps.iter().last()
|
||||
let largest_timestamp = timestamps
|
||||
.iter()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow::anyhow!("No valid timestamps found"))?;
|
||||
|
||||
|
||||
// Create version string from package names
|
||||
let version = packages.iter().fold("".to_string(), |mut s, n| {
|
||||
if !s.is_empty() {
|
||||
|
|
@ -71,7 +84,7 @@ fn dpkg_parse_metadata(packages: &BTreeSet<String>) -> Result<ContentMetadata> {
|
|||
s.push_str(n);
|
||||
s
|
||||
});
|
||||
|
||||
|
||||
Ok(ContentMetadata {
|
||||
timestamp: *largest_timestamp,
|
||||
version,
|
||||
|
|
@ -86,40 +99,67 @@ pub(crate) fn query_files<T>(
|
|||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
if sysroot_path.is_empty() {
|
||||
bail!("sysroot_path cannot be empty");
|
||||
}
|
||||
|
||||
let mut packages = BTreeSet::new();
|
||||
let paths: Vec<_> = paths.into_iter().collect();
|
||||
|
||||
if paths.is_empty() {
|
||||
bail!("No paths provided to query");
|
||||
}
|
||||
|
||||
log::debug!("Querying dpkg database for {} paths in sysroot: {}", paths.len(), sysroot_path);
|
||||
|
||||
for path in &paths {
|
||||
// Use dpkg -S to find which package owns the file
|
||||
let mut cmd = std::process::Command::new("dpkg");
|
||||
cmd.args(["-S", "--root", sysroot_path]);
|
||||
cmd.arg(path.as_ref());
|
||||
|
||||
let dpkgout = cmd.output()?;
|
||||
if !dpkgout.status.success() {
|
||||
// Skip files that don't belong to any package
|
||||
let path_ref = path.as_ref();
|
||||
if path_ref.to_string_lossy().is_empty() {
|
||||
log::warn!("Skipping empty path");
|
||||
continue;
|
||||
}
|
||||
|
||||
let package = parse_dpkg_s_output(&dpkgout.stdout)?;
|
||||
// Use dpkg -S to find which package owns the file
|
||||
let mut cmd = std::process::Command::new("dpkg");
|
||||
cmd.args(["-S", "--root", sysroot_path]);
|
||||
cmd.arg(path_ref);
|
||||
|
||||
let dpkgout = cmd.output()
|
||||
.with_context(|| format!("Failed to execute dpkg command for path: {:?}", path_ref))?;
|
||||
|
||||
if !dpkgout.status.success() {
|
||||
// Skip files that don't belong to any package
|
||||
log::debug!("File {:?} does not belong to any package (dpkg exit code: {})",
|
||||
path_ref, dpkgout.status);
|
||||
continue;
|
||||
}
|
||||
|
||||
let package = parse_dpkg_s_output(&dpkgout.stdout)
|
||||
.with_context(|| format!("Failed to parse dpkg output for path: {:?}", path_ref))?;
|
||||
packages.insert(package);
|
||||
}
|
||||
|
||||
|
||||
if packages.is_empty() {
|
||||
log::debug!("No packages found with --root, trying local system");
|
||||
// If no packages found, try without --root for local system
|
||||
for path in &paths {
|
||||
let path_ref = path.as_ref();
|
||||
let mut cmd = std::process::Command::new("dpkg");
|
||||
cmd.args(["-S"]);
|
||||
cmd.arg(path.as_ref());
|
||||
|
||||
let dpkgout = cmd.output()?;
|
||||
cmd.arg(path_ref);
|
||||
|
||||
let dpkgout = cmd.output()
|
||||
.with_context(|| format!("Failed to execute local dpkg command for path: {:?}", path_ref))?;
|
||||
|
||||
if dpkgout.status.success() {
|
||||
let package = parse_dpkg_s_output(&dpkgout.stdout)?;
|
||||
let package = parse_dpkg_s_output(&dpkgout.stdout)
|
||||
.with_context(|| format!("Failed to parse local dpkg output for path: {:?}", path_ref))?;
|
||||
packages.insert(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log::debug!("Found {} packages for {} paths", packages.len(), paths.len());
|
||||
dpkg_parse_metadata(&packages)
|
||||
}
|
||||
|
||||
|
|
@ -128,26 +168,45 @@ fn test_parse_dpkg_s_output() {
|
|||
let testdata = "grub-efi-amd64:amd64: /usr/lib/grub/x86_64-efi";
|
||||
let parsed = parse_dpkg_s_output(testdata.as_bytes()).unwrap();
|
||||
assert_eq!(parsed, "grub-efi-amd64:amd64");
|
||||
|
||||
|
||||
let testdata2 = "shim-signed: /usr/lib/shim/shimx64.efi";
|
||||
let parsed2 = parse_dpkg_s_output(testdata2.as_bytes()).unwrap();
|
||||
assert_eq!(parsed2, "shim-signed");
|
||||
|
||||
|
||||
// Test with different package name format
|
||||
let testdata3 = "grub-efi-amd64: /usr/lib/grub/x86_64-efi";
|
||||
let parsed3 = parse_dpkg_s_output(testdata3.as_bytes()).unwrap();
|
||||
assert_eq!(parsed3, "grub-efi-amd64");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dpkg_s_output_errors() {
|
||||
// Test invalid UTF-8
|
||||
let invalid_utf8 = b"package\xff: /path";
|
||||
assert!(parse_dpkg_s_output(invalid_utf8).is_err());
|
||||
|
||||
// Test empty package name
|
||||
let empty_package = ": /path";
|
||||
assert!(parse_dpkg_s_output(empty_package.as_bytes()).is_err());
|
||||
|
||||
// Test no separator
|
||||
let no_separator = "package /path";
|
||||
assert!(parse_dpkg_s_output(no_separator.as_bytes()).is_err());
|
||||
|
||||
// Test only colon
|
||||
let only_colon = ":";
|
||||
assert!(parse_dpkg_s_output(only_colon.as_bytes()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dpkg_parse_metadata() {
|
||||
// Mock package installation times for testing
|
||||
let mock_time = Utc::now();
|
||||
|
||||
|
||||
let mut packages = BTreeSet::new();
|
||||
packages.insert("grub-efi-amd64:amd64".to_string());
|
||||
packages.insert("shim-signed".to_string());
|
||||
|
||||
|
||||
// For testing, we'll create a mock version that doesn't depend on actual files
|
||||
let version = packages.iter().fold("".to_string(), |mut s, n| {
|
||||
if !s.is_empty() {
|
||||
|
|
@ -156,17 +215,19 @@ fn test_dpkg_parse_metadata() {
|
|||
s.push_str(n);
|
||||
s
|
||||
});
|
||||
|
||||
|
||||
// Create a mock ContentMetadata for testing
|
||||
let mock_metadata = ContentMetadata {
|
||||
timestamp: mock_time,
|
||||
version,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
mock_metadata.version,
|
||||
"grub-efi-amd64:amd64,shim-signed"
|
||||
);
|
||||
|
||||
assert_eq!(mock_metadata.version, "grub-efi-amd64:amd64,shim-signed");
|
||||
// Timestamp should be recent
|
||||
assert!(mock_metadata.timestamp > DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z").unwrap().with_timezone(&Utc));
|
||||
assert!(
|
||||
mock_metadata.timestamp
|
||||
> DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue