Add sqlite for tracking build history.

This commit is contained in:
Tom Alexander
2026-02-14 22:15:09 -05:00
parent 0e0c5dac80
commit 7eb36ce0a4
16 changed files with 442 additions and 30 deletions

View File

@@ -1,18 +1,32 @@
use crate::Result;
use crate::cli::parameters::BuildArgs;
use crate::config::Config;
use crate::config::TargetConfig;
use crate::error::CustomError;
use crate::database::db_handle::DbHandle;
use crate::fs_util::assert_directory;
use crate::fs_util::is_git_repo;
use crate::git_util::git_force_into_state;
use crate::git_util::git_init_at_rev;
use crate::nix_util::nixos_build_target;
pub(crate) async fn run_build(args: BuildArgs) -> Result<(), CustomError> {
pub(crate) async fn run_build(args: BuildArgs) -> Result<()> {
println!("{:?}", args);
let config = Config::load_from_file(args.config).await?;
println!("{:?}", config);
let database_path = config.get_database_path()?;
let database_parent = database_path
.parent()
.expect("Database should exist in a folder.");
let database_path = database_path.to_string_lossy();
assert_directory!(
database_parent,
"Creating database directory {}",
database_parent.to_string_lossy()
);
let db_handle = DbHandle::new(Some(database_path)).await?;
for target_name in args.target {
let target_config = {
let target_config = config.get_target_config(&target_name)?;
@@ -30,15 +44,12 @@ pub(crate) async fn run_build(args: BuildArgs) -> Result<(), CustomError> {
Ok(())
}
async fn prepare_flake_repo(
config_root: &Config,
target_config: &TargetConfig,
) -> Result<(), CustomError> {
async fn prepare_flake_repo(config_root: &Config, target_config: &TargetConfig) -> Result<()> {
let repo_directory = target_config.get_repo_directory(config_root)?;
assert_directory!(
&repo_directory,
"Creating repo directory {}",
(&repo_directory).to_string_lossy()
repo_directory.to_string_lossy()
);
if is_git_repo(&repo_directory).await? {
@@ -63,16 +74,13 @@ async fn prepare_flake_repo(
Ok(())
}
async fn build_target(
config_root: &Config,
target_config: &TargetConfig,
) -> Result<(), CustomError> {
async fn build_target(config_root: &Config, target_config: &TargetConfig) -> Result<()> {
let flake_directory = target_config.get_flake_directory(config_root)?;
let build_directory = target_config.get_build_directory(config_root)?;
assert_directory!(
&build_directory,
"Creating build directory {}",
(&build_directory).to_string_lossy()
build_directory.to_string_lossy()
);
nixos_build_target(build_directory, flake_directory, target_config.get_attr()?).await?;

View File

@@ -73,4 +73,11 @@ impl Config {
let work_dir = current_dir.join("work");
Ok(Cow::Owned(work_dir))
}
/// The path to the sqlite database where run history is stored.
pub(crate) fn get_database_path(&self) -> Result<Cow<'_, Path>, CustomError> {
let output_directory = self.get_output_directory()?;
let database_path = output_directory.join("nix_builder.sqlite");
Ok(Cow::Owned(database_path))
}
}

80
src/database/db_handle.rs Normal file
View File

@@ -0,0 +1,80 @@
use sqlx::Executor;
use sqlx::Pool;
use sqlx::Sqlite;
use sqlx::migrate::MigrateDatabase;
use sqlx::sqlite::SqlitePoolOptions;
use tracing::info;
use tracing::warn;
use super::migration::run_migrations;
use crate::Result;
pub(crate) struct DbHandle {
pub(crate) conn: Pool<Sqlite>,
}
impl DbHandle {
pub(crate) async fn new<P: AsRef<str>>(db_path: Option<P>) -> Result<DbHandle> {
let db_path = db_path.as_ref().map(|p| p.as_ref());
let options = SqlitePoolOptions::new()
.max_connections(5)
.test_before_acquire(true)
.after_connect(|conn, _meta| {
Box::pin(async move {
// Enforce foreign keys.
conn.execute("PRAGMA foreign_keys = ON;").await?;
// Allows writes at the same time as reads.
conn.execute("PRAGMA journal_mode = WAL;").await?;
// Do not sync to disk after *every* write.
conn.execute("PRAGMA synchronous = NORMAL;").await?;
// Keep 10k database pages in memory (~40MiB).
conn.execute("PRAGMA cache_size = 10000;").await?;
// Stores temporary tables, indexes, and sorting operations in memory.
conn.execute("PRAGMA temp_store = MEMORY;").await?;
// Use mmap to access database.
conn.execute("PRAGMA mmap_size = 268435456;").await?;
// Clear space of deleted rows at transaction end.
conn.execute("PRAGMA auto_vacuum = FULL;").await?;
Ok(())
})
})
.after_release(|conn, _meta| {
Box::pin(async move {
// Attempt to optimize the database. (Currently just runs ANALYZE)
conn.execute("PRAGMA optimize;").await?;
// Rebuild the DB file which defragments and clears out deleted pages.
conn.execute("VACUUM;").await?;
Ok(true)
})
});
let conn = match db_path {
Some(path) => {
info!("Connecting to sqlite database at {path}");
if !Sqlite::database_exists(path).await.unwrap_or(false) {
info!("Creating a new sqlite database at {path}");
Sqlite::create_database(path).await.unwrap();
} else {
info!("Connecting to existing sqlite database at {path}");
}
let full_url = format!("sqlite:{path}");
options.connect(&full_url).await?
}
None => {
warn!("No sqlite_path set in config. Using an in-memory database.");
// We force it to a single connection that never dies or else the data and schema in the in-memory DB is lost.
options
.min_connections(1)
.max_connections(1)
.idle_timeout(None)
.max_lifetime(None)
.connect("sqlite::memory:")
.await?
}
};
run_migrations(&conn).await?;
Ok(DbHandle { conn })
}
}

15
src/database/migration.rs Normal file
View File

@@ -0,0 +1,15 @@
use std::ops::Deref;
use sqlx::Acquire;
use sqlx::migrate::Migrate;
use crate::Result;
pub(crate) async fn run_migrations<'a, A>(db: A) -> Result<()>
where
A: Acquire<'a>,
<A::Connection as Deref>::Target: Migrate,
{
sqlx::migrate!("./migrations").run(db).await?;
Ok(())
}

2
src/database/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub(crate) mod db_handle;
pub(crate) mod migration;

View File

@@ -14,6 +14,8 @@ pub(crate) enum CustomError {
FromUtf8(#[allow(dead_code)] FromUtf8Error),
PathStripPrefix(#[allow(dead_code)] std::path::StripPrefixError),
UrlParseError(#[allow(dead_code)] url::ParseError),
Migrate(#[allow(dead_code)] sqlx::migrate::MigrateError),
Sql(#[allow(dead_code)] sqlx::Error),
}
impl From<std::io::Error> for CustomError {
@@ -81,3 +83,15 @@ impl From<url::ParseError> for CustomError {
CustomError::UrlParseError(value)
}
}
impl From<sqlx::migrate::MigrateError> for CustomError {
fn from(value: sqlx::migrate::MigrateError) -> Self {
CustomError::Migrate(value)
}
}
impl From<sqlx::Error> for CustomError {
fn from(value: sqlx::Error) -> Self {
CustomError::Sql(value)
}
}

View File

@@ -23,10 +23,7 @@ use crate::error::CustomError;
pub(crate) async fn is_directory<D: AsRef<Path>>(dir: D) -> Result<bool, CustomError> {
let metadata = tokio::fs::metadata(dir).await;
let result = match metadata {
Ok(metadata) if metadata.is_dir() => true,
_ => false,
};
let result = matches!(metadata, Ok(metadata) if metadata.is_dir());
Ok(result)
}

View File

@@ -15,7 +15,7 @@ where
let dest = AsRef::<OsStr>::as_ref(dest.as_ref());
command.arg("-C");
command.arg(dest);
command.args(&["init"]);
command.args(["init"]);
command.arg(format!("--initial-branch={}", branch.as_ref()));
command.kill_on_drop(true);
let output = command.output().await?;
@@ -43,7 +43,7 @@ where
let mut command = Command::new("git");
command.arg("-C");
command.arg(dest);
command.args(&["remote", "add"]);
command.args(["remote", "add"]);
command.arg(name);
command.arg(url);
command.kill_on_drop(true);
@@ -78,7 +78,7 @@ where
let mut command = Command::new("git");
command.arg("-C");
command.arg(dest);
command.args(&["fetch"]);
command.args(["fetch"]);
if let Some(d) = depth {
command.arg(format!("--depth={}", d));
}
@@ -118,7 +118,7 @@ where
let mut command = Command::new("git");
command.arg("-C");
command.arg(dest);
command.args(&["checkout"]);
command.args(["checkout"]);
if force {
command.arg("--force");
}
@@ -149,7 +149,7 @@ where
let mut command = Command::new("git");
command.arg("-C");
command.arg(dest);
command.args(&["remote", "get-url"]);
command.args(["remote", "get-url"]);
command.arg(remote.as_ref());
command.kill_on_drop(true);
let output = command.output().await?;
@@ -181,7 +181,7 @@ where
let mut command = Command::new("git");
command.arg("-C");
command.arg(dest);
command.args(&["reset"]);
command.args(["reset"]);
if hard {
command.arg("--hard");
}
@@ -213,7 +213,7 @@ where
let mut command = Command::new("git");
command.arg("-C");
command.arg(dest);
command.args(&["clean"]);
command.args(["clean"]);
if recurse_into_untracked_directories {
command.arg("-d");
}

View File

@@ -12,18 +12,21 @@ use self::init_tracing::shutdown_telemetry;
mod cli;
mod command;
mod config;
mod database;
mod error;
mod fs_util;
mod git_util;
mod init_tracing;
mod nix_util;
fn main() -> Result<ExitCode, CustomError> {
pub(crate) type Result<T> = std::result::Result<T, CustomError>;
fn main() -> Result<ExitCode> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { main_body().await })
}
async fn main_body() -> Result<ExitCode, CustomError> {
async fn main_body() -> Result<ExitCode> {
init_telemetry().expect("Telemetry should initialize successfully.");
let args = Cli::parse();
match args.command {

View File

@@ -29,7 +29,7 @@ where
// nixos-rebuild build --show-trace --sudo --max-jobs "$JOBS" --flake "$DIR/../../#odo" --log-format internal-json -v "${@}"
let mut command = Command::new("nixos-rebuild");
command.current_dir(build_path);
command.args(&[
command.args([
"build",
"--show-trace",
"--sudo",