[DetSys#1143] repair: add sequoia subcommand to migrate build users to the new 351+ UID range

* repair: use target_lexicon::OperatingSystem over target_os cfg

* repair: make repair actions a collection

* Make some things pub for ease of reuse

* fixup: make write_receipt() take a reference instead of ownership

* fixup: make write_receipt() atomic and member of InstallPlan

* CreateUser: enable skipping the completion check

This is useful for when you don't care if it's been completed or not and
want to rerun the commands. Especially useful on macOS, where `dscl .
-create` is idempotent.

* repair: add `sequoia` subcommand that can migrate build users to the new 351+ UID range

* fixup: should not be able to specify uid base

* fixup: nicer wording for human consumption

* fixup: don't worry about incompatible receipts

* fixup: prompt before some repair commands

* fixup: set user_base outside of branch

* fixup: store a timestamped, pre-repair copy of the receipt

* fixup: note whether or not the receipt will be updated

* fixup: note that uninstallation will work even if the receipt could not be updated

(cherry picked from commit ded6eb7352eaf1bf9dcd07719a13c5b3f083a739)
Upstream-PR: https://github.com/DeterminateSystems/nix-installer/pull/1143
Change-Id: I9084dcf5a53b1453436db6fedbe5e785a8b5e3ae
This commit is contained in:
Cole Helbling 2024-09-12 08:09:29 -07:00 committed by emilazy
parent 7c00e44b14
commit be67a8a4e2
7 changed files with 611 additions and 81 deletions

View file

@ -15,10 +15,10 @@ Create an operating system level user in the given group
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct AddUserToGroup {
name: String,
uid: u32,
groupname: String,
gid: u32,
pub(crate) name: String,
pub(crate) uid: u32,
pub(crate) groupname: String,
pub(crate) gid: u32,
}
impl AddUserToGroup {

View file

@ -13,10 +13,10 @@ Create an operating system level user in the given group
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateUser {
name: String,
uid: u32,
groupname: String,
gid: u32,
pub(crate) name: String,
pub(crate) uid: u32,
pub(crate) groupname: String,
pub(crate) gid: u32,
comment: String,
}
@ -28,6 +28,7 @@ impl CreateUser {
groupname: String,
gid: u32,
comment: String,
check_completed: bool,
) -> Result<StatefulAction<Self>, ActionError> {
let this = Self {
name: name.clone(),
@ -49,7 +50,8 @@ impl CreateUser {
},
}
// Ensure user does not exists
if check_completed {
// Ensure user does not exist
if let Some(user) = User::from_name(name.as_str())
.map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e))
.map_err(Self::error)?
@ -73,6 +75,7 @@ impl CreateUser {
tracing::debug!("Creating user `{}` already complete", this.name);
return Ok(StatefulAction::completed(this));
}
}
Ok(StatefulAction::uncompleted(this))
}

View file

@ -9,14 +9,14 @@ use tracing::{span, Span};
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateUsersAndGroups {
nix_build_user_count: u32,
nix_build_group_name: String,
nix_build_group_id: u32,
nix_build_user_prefix: String,
nix_build_user_id_base: u32,
create_group: StatefulAction<CreateGroup>,
create_users: Vec<StatefulAction<CreateUser>>,
add_users_to_groups: Vec<StatefulAction<AddUserToGroup>>,
pub(crate) nix_build_group_name: String,
pub(crate) nix_build_group_id: u32,
pub(crate) nix_build_user_count: u32,
pub(crate) nix_build_user_prefix: String,
pub(crate) nix_build_user_id_base: u32,
pub(crate) create_group: StatefulAction<CreateGroup>,
pub(crate) create_users: Vec<StatefulAction<CreateUser>>,
pub(crate) add_users_to_groups: Vec<StatefulAction<AddUserToGroup>>,
}
impl CreateUsersAndGroups {
@ -36,6 +36,7 @@ impl CreateUsersAndGroups {
settings.nix_build_group_name.clone(),
settings.nix_build_group_id,
format!("Nix build user {index}"),
true,
)
.await
.map_err(Self::error)?,

View file

@ -274,7 +274,7 @@ impl ActionDescription {
}
/// A 'tag' name an action has that corresponds to the one we serialize in [`typetag]`
pub struct ActionTag(&'static str);
pub struct ActionTag(pub &'static str);
impl std::fmt::Display for ActionTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View file

@ -1,14 +1,29 @@
use std::io::IsTerminal as _;
use std::process::ExitCode;
use std::time::SystemTime;
use crate::{
action::common::ConfigureShellProfile,
cli::{ensure_root, CommandExecute},
planner::{PlannerError, ShellProfileLocations},
};
use clap::{ArgAction, Parser};
use clap::{ArgAction, Parser, Subcommand};
use eyre::Context as _;
use serde::{Deserialize, Serialize};
use target_lexicon::OperatingSystem;
use tokio::process::Command;
use crate::action::base::{AddUserToGroup, CreateGroup, CreateUser};
use crate::action::common::{ConfigureShellProfile, CreateUsersAndGroups};
use crate::action::{Action, ActionState, StatefulAction};
use crate::cli::interaction::PromptChoice;
use crate::cli::{ensure_root, CommandExecute};
use crate::plan::RECEIPT_LOCATION;
use crate::planner::{PlannerError, ShellProfileLocations};
use crate::{execute_command, InstallPlan};
/// The base UID that we temporarily move build users to while migrating macOS to the new range.
const TEMP_USER_ID_BASE: u32 = 31000;
/**
Update the shell profiles to make Nix usable after system upgrades.
Various actions to repair Nix installations.
The default is to repair shell hooks.
*/
#[derive(Debug, Parser)]
#[command(args_conflicts_with_subcommands = true)]
@ -21,40 +36,539 @@ pub struct Repair {
global = true
)]
pub no_confirm: bool,
#[command(subcommand)]
command: Option<RepairKind>,
}
#[derive(Clone, Debug, Subcommand, serde::Deserialize, serde::Serialize)]
pub enum RepairKind {
/// Update the shell profiles to make Nix usable after system upgrades.
Hooks,
/// Recover from the macOS 15 Sequoia update taking over _nixbld users.
///
/// Default functionality is to only attempt the fix if _nixbld users are missing.
///
/// Can be run before taking a macOS 15 Sequoia update by passing the `--move-existing-users`
/// flag (which will move the Nix build users to the new UID range even if they all currently
/// exist).
Sequoia {
/// The Nix build user prefix (user numbers will be postfixed)
#[cfg_attr(
feature = "cli",
clap(long, env = "NIX_INSTALLER_NIX_BUILD_USER_PREFIX", global = true)
)]
#[cfg_attr(
all(target_os = "macos", feature = "cli"),
clap(default_value = "_nixbld")
)]
#[cfg_attr(
all(target_os = "linux", feature = "cli"),
clap(default_value = "nixbld")
)]
nix_build_user_prefix: String,
/// The number of build users to ensure exist
#[cfg_attr(
feature = "cli",
clap(
long,
alias = "daemon-user-count",
env = "NIX_INSTALLER_NIX_BUILD_USER_COUNT",
default_value = "32",
global = true
)
)]
nix_build_user_count: u32,
/// The Nix build group name
#[cfg_attr(
feature = "cli",
clap(
long,
default_value = "nixbld",
env = "NIX_INSTALLER_NIX_BUILD_GROUP_NAME",
global = true
)
)]
nix_build_group_name: String,
/// If `nix-installer` should move the build users to a Sequoia-compatible range, even when
/// they all currently exist
#[cfg_attr(
feature = "cli",
clap(
long,
action(ArgAction::SetTrue),
default_value = "false",
global = true,
env = "NIX_INSTALLER_MOVE_EXISTING_USERS"
)
)]
move_existing_users: bool,
},
}
impl Repair {
pub fn command(&self) -> RepairKind {
self.command.to_owned().unwrap_or(RepairKind::Hooks)
}
}
#[async_trait::async_trait]
impl CommandExecute for Repair {
#[tracing::instrument(level = "trace", skip_all)]
async fn execute(self) -> eyre::Result<ExitCode> {
let Self { no_confirm: _ } = self;
let command = self.command();
ensure_root()?;
let mut reconfigure = ConfigureShellProfile::plan(ShellProfileLocations::default())
.await
.map_err(PlannerError::Action)?
.boxed();
let mut repair_actions = Vec::new();
let (prompt_before_repairing, brief_repair_summary) = match command {
RepairKind::Hooks => (
false,
String::from("Will ensure the Nix shell profiles are still being sourced"),
),
RepairKind::Sequoia {
ref nix_build_user_prefix,
nix_build_user_count,
ref nix_build_group_name,
..
} => {
let maybe_users_and_groups_from_receipt = maybe_users_and_groups_from_receipt(
nix_build_user_prefix,
nix_build_user_count,
nix_build_group_name,
)
.await?;
if let Err(err) = reconfigure.try_execute().await {
println!("{:#?}", err);
return Ok(ExitCode::FAILURE);
}
// TODO: Using `cfg` based on OS is not a long term solution.
// Make this read the planner from the `/nix/receipt.json` to determine which tasks to run.
#[cfg(target_os = "macos")]
let user_base = crate::settings::default_nix_build_user_id_base();
let brief_summary = format!(
"Will move the {nix_build_user_prefix} users to the Sequoia-compatible \
{user_base}+ UID range and {maybe_update_receipt} update the receipt",
maybe_update_receipt = if maybe_users_and_groups_from_receipt
.receipt_action_idx_create_group
.is_some()
{
let mut reconfigure = crate::action::macos::ConfigureRemoteBuilding::plan()
"WILL"
} else {
"WILL NOT"
}
);
(!self.no_confirm, brief_summary)
},
};
if prompt_before_repairing {
loop {
match crate::cli::interaction::prompt(
&brief_repair_summary,
"Proceed?",
PromptChoice::Yes,
true,
)
.await?
{
PromptChoice::Yes => break,
PromptChoice::No => {
crate::cli::interaction::clean_exit_with_message(
"Okay, didn't do anything! Bye!",
)
.await
},
PromptChoice::Explain => (),
}
}
} else {
tracing::info!("{}", brief_repair_summary);
}
// TODO(cole-h): if we add another repair command, make this whole thing more generic
let updated_receipt = match command.clone() {
RepairKind::Hooks => {
let reconfigure = ConfigureShellProfile::plan(ShellProfileLocations::default())
.await
.map_err(PlannerError::Action)?
.boxed();
repair_actions.push(reconfigure);
if let Err(err) = reconfigure.try_execute().await {
match OperatingSystem::host() {
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin => {
let reconfigure = crate::action::macos::ConfigureRemoteBuilding::plan()
.await
.map_err(PlannerError::Action)?
.boxed();
repair_actions.push(reconfigure);
},
_ => {
// Linux-specific hook repair actions, once we have them
},
}
None
},
RepairKind::Sequoia {
nix_build_user_prefix,
nix_build_user_count,
nix_build_group_name,
move_existing_users,
} => {
if !matches!(
OperatingSystem::host(),
OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin
) {
return Err(color_eyre::eyre::eyre!(
"The `sequoia` repair command is only available on macOS"
));
}
if !std::io::stdin().is_terminal() && !self.no_confirm {
return Err(color_eyre::eyre::eyre!(
"The `sequoia` repair command should be run in an interactive terminal. If \
you accept the risks of an unattended repair, pass `--no-confirm`."
));
}
let user_base = crate::settings::default_nix_build_user_id_base();
let maybe_users_and_groups_from_receipt = maybe_users_and_groups_from_receipt(
&nix_build_user_prefix,
nix_build_user_count,
&nix_build_group_name,
)
.await?;
let user_prefix = maybe_users_and_groups_from_receipt.user_prefix;
let user_count = maybe_users_and_groups_from_receipt.user_count;
let group_name = maybe_users_and_groups_from_receipt.group_name;
let group_gid = maybe_users_and_groups_from_receipt.group_gid;
let receipt_action_idx_create_group =
maybe_users_and_groups_from_receipt.receipt_action_idx_create_group;
if receipt_action_idx_create_group.is_none() {
tracing::warn!(
"Unable to find {} in receipt (receipt didn't exist or is unable to be \
parsed by this version of the installer). Your receipt at {RECEIPT_LOCATION} \
will not reflect the changed UIDs, but the users will still be relocated \
to the new Sequoia-compatible UID range, starting at {user_base}, and \
uninstallation will continue to work as normal, even if the UIDs do not match.",
CreateUsersAndGroups::action_tag()
);
}
let group_plist = {
let buf = execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
.args(["-plist", ".", "-read", &format!("/Groups/{group_name}")])
.stdin(std::process::Stdio::null()),
)
.await?
.stdout;
let group_plist: GroupPlist = plist::from_bytes(&buf)?;
group_plist
};
let expected_users = group_plist
.group_membership
.into_iter()
.enumerate()
.map(|(idx, name)| ((idx + 1) as u32, name))
.collect::<Vec<_>>();
let mut missing_users = Vec::new();
for (user_idx, user_name) in &expected_users {
let ret = execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
.args([".", "-read", &format!("/Users/{user_name}")])
.stdin(std::process::Stdio::null()),
)
.await;
if let Err(e) = ret {
tracing::debug!(%e, user_name, "Couldn't read user, assuming it's missing");
missing_users.push((user_idx, user_name));
}
}
if missing_users.is_empty() && !move_existing_users {
tracing::info!("Nothing to do! All users appear to be in place!");
return Ok(ExitCode::SUCCESS);
}
let mut existing_users = expected_users.clone();
existing_users.retain(|(idx, _name)| {
!missing_users.iter().any(|(idx2, _name2)| idx == *idx2)
});
// NOTE(coleh-h): We move all existing build users into a temp UID range in case a
// user customized the number of users they created and the UIDs would overlap in
// this new range, i.e. with 128 build users, _nixbld81 prior to migration would
// have the same ID as _nixbld31 after the migration and would likely fail.
for (user_idx, user_name) in existing_users {
let temp_user_id = TEMP_USER_ID_BASE + user_idx;
execute_command(
Command::new("/usr/bin/dscl")
.process_group(0)
// NOTE(cole-h): even though it says "create" it's really "create-or-update"
.args([".", "-create", &format!("/Users/{user_name}"), "UniqueID"])
.arg(temp_user_id.to_string())
.stdin(std::process::Stdio::null()),
)
.await?;
}
let mut create_users = Vec::with_capacity(user_count as usize);
let group_gid = group_gid.unwrap_or(group_plist.gid);
for (idx, name) in expected_users {
let create_user = CreateUser::plan(
name,
user_base + idx,
group_name.clone(),
group_gid,
format!("Nix build user {idx}"),
false,
)
.await?;
create_users.push(create_user);
}
let mut maybe_updated_receipt = None;
if let Some((mut receipt, action_idx, create_group)) =
receipt_action_idx_create_group
{
// NOTE(cole-h): Once we write the updated receipt, these steps will have been
// completed, so manually setting them to completed with
// StatefulAction::completed is fine.
let (add_users_to_groups, create_users): (
Vec<StatefulAction<AddUserToGroup>>,
Vec<StatefulAction<CreateUser>>,
) = create_users
.iter()
.cloned()
.map(|create_user| {
let action = create_user.action;
(
StatefulAction::completed(AddUserToGroup {
name: action.name.clone(),
uid: action.uid,
groupname: action.groupname.clone(),
gid: action.gid,
}),
StatefulAction::completed(action),
)
})
.unzip();
let create_users_and_groups = StatefulAction::completed(CreateUsersAndGroups {
nix_build_group_name: group_name.clone(),
nix_build_group_id: group_gid,
nix_build_user_count: user_count,
nix_build_user_prefix: user_prefix.clone(),
nix_build_user_id_base: user_base,
create_group,
create_users: create_users.clone(),
add_users_to_groups,
});
let _replaced = std::mem::replace(
&mut receipt.actions[action_idx],
create_users_and_groups.boxed(),
);
maybe_updated_receipt = Some(receipt);
}
let create_users = create_users
.into_iter()
.map(|create_user| create_user.boxed())
.collect::<Vec<_>>();
repair_actions.extend(create_users);
maybe_updated_receipt
},
};
for mut action in repair_actions {
if let Err(err) = action.try_execute().await {
println!("{:#?}", err);
return Ok(ExitCode::FAILURE);
}
action.state = ActionState::Completed;
}
if let Some(updated_receipt) = updated_receipt {
let timestamp_millis = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_millis();
let mut old_receipt = std::path::PathBuf::from(RECEIPT_LOCATION);
old_receipt.set_extension(format!("pre-repair.{timestamp_millis}.json"));
tokio::fs::copy(RECEIPT_LOCATION, &old_receipt).await?;
tracing::info!("Backed up pre-repair receipt to {}", old_receipt.display());
updated_receipt.write_receipt().await?;
tracing::info!("Wrote updated receipt");
}
tracing::info!("Finished repairing successfully!");
Ok(ExitCode::SUCCESS)
}
}
#[derive(Serialize, Deserialize)]
/// Structured output of `dscl -plist . -read /Groups/{name}`
struct GroupPlist {
#[serde(rename = "dsAttrTypeStandard:GroupMembership")]
group_membership: Vec<String>,
#[serde(
rename = "dsAttrTypeStandard:PrimaryGroupID",
deserialize_with = "deserialize_gid"
)]
gid: u32,
}
pub fn deserialize_gid<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s: Vec<String> = serde::Deserialize::deserialize(deserializer)?;
let gid_str = s
.first()
.ok_or_else(|| serde::de::Error::invalid_length(0, &"a gid entry"))?;
let gid: u32 = gid_str.parse().map_err(serde::de::Error::custom)?;
Ok(gid)
}
#[tracing::instrument]
async fn get_existing_receipt() -> Option<InstallPlan> {
match std::path::Path::new(RECEIPT_LOCATION).exists() {
true => {
tracing::debug!("Reading existing receipt");
let install_plan_string = tokio::fs::read_to_string(RECEIPT_LOCATION).await.ok();
match install_plan_string {
Some(s) => match serde_json::from_str::<InstallPlan>(s.as_str()) {
Ok(plan) => {
tracing::debug!(plan_version = %plan.version, "Able to parse receipt");
Some(plan)
},
Err(e) => {
tracing::debug!(?e);
tracing::warn!("Could not parse receipt. Your receipt will not be updated to account for the new UIDs");
None
},
},
_ => None,
}
},
false => None,
}
}
#[tracing::instrument(skip_all)]
fn find_users_and_groups(
existing_receipt: Option<InstallPlan>,
) -> color_eyre::Result<Option<(InstallPlan, usize, CreateUsersAndGroups)>> {
let ret = match existing_receipt {
Some(receipt) => {
tracing::debug!("Got existing receipt");
let mut maybe_create_users_and_groups_idx_action = None;
for (idx, stateful_action) in receipt.actions.iter().enumerate() {
let action_tag = stateful_action.inner_typetag_name();
tracing::trace!("Found {action_tag} in receipt");
if action_tag == CreateUsersAndGroups::action_tag().0 {
tracing::debug!(
"Found {} in receipt, preparing to roundtrip to extract the real type",
CreateUsersAndGroups::action_tag().0
);
// NOTE(cole-h): this round-trip is kinda jank... but Action is not
// object-safe, and I can't think of any other way to get the
// concrete `CreateUsersAndGroups` type out of a `Box<dyn Action>`.
let action = &stateful_action.action;
let create_users_and_groups_json =
serde_json::to_string(action).with_context(|| {
format!("round-tripping {action_tag} json to extract real type")
})?;
let create_users_and_groups: CreateUsersAndGroups =
serde_json::from_str(&create_users_and_groups_json).with_context(|| {
format!("round-tripping {action_tag} json to extract real type")
})?;
maybe_create_users_and_groups_idx_action =
Some((receipt, idx, create_users_and_groups));
break;
}
}
maybe_create_users_and_groups_idx_action
},
None => {
tracing::debug!(
"Receipt didn't exist or is unable to be parsed by this version of the installer"
);
None
},
};
Ok(ret)
}
struct UsersAndGroupsMeta {
user_prefix: String,
user_count: u32,
group_name: String,
group_gid: Option<u32>,
receipt_action_idx_create_group: Option<(InstallPlan, usize, StatefulAction<CreateGroup>)>,
}
async fn maybe_users_and_groups_from_receipt(
nix_build_user_prefix: &str,
nix_build_user_count: u32,
nix_build_group_name: &str,
) -> eyre::Result<UsersAndGroupsMeta> {
let existing_receipt = get_existing_receipt().await;
let maybe_create_users_and_groups_idx_action = find_users_and_groups(existing_receipt)?;
match maybe_create_users_and_groups_idx_action {
Some((receipt, create_users_and_groups_idx, action)) => {
tracing::debug!("Found {} in receipt", CreateUsersAndGroups::action_tag());
let user_prefix = action.nix_build_user_prefix;
let user_count = action.nix_build_user_count;
let group_gid = action.nix_build_group_id;
let group_name = action.nix_build_group_name;
Ok(UsersAndGroupsMeta {
user_prefix,
user_count,
group_name,
group_gid: Some(group_gid),
receipt_action_idx_create_group: Some((
receipt,
create_users_and_groups_idx,
action.create_group,
)),
})
},
None => Ok(UsersAndGroupsMeta {
user_prefix: nix_build_user_prefix.to_string(),
user_count: nix_build_user_count,
group_name: nix_build_group_name.to_string(),
group_gid: None,
receipt_action_idx_create_group: None,
}),
}
}

View file

@ -15,7 +15,7 @@ pub const RECEIPT_LOCATION: &str = "/nix/receipt.json";
A set of [`Action`]s, along with some metadata, which can be carried out to drive an install or
revert
*/
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct InstallPlan {
pub(crate) version: Version,
@ -156,7 +156,7 @@ impl InstallPlan {
if cancel_channel.try_recv()
!= Err(tokio::sync::broadcast::error::TryRecvError::Empty)
{
if let Err(err) = write_receipt(self.clone()).await {
if let Err(err) = self.write_receipt().await {
tracing::error!("Error saving receipt: {:?}", err);
}
@ -166,7 +166,7 @@ impl InstallPlan {
tracing::info!("Step: {}", action.tracing_synopsis());
if let Err(err) = action.try_execute().await {
if let Err(err) = write_receipt(self.clone()).await {
if let Err(err) = self.write_receipt().await {
tracing::error!("Error saving receipt: {:?}", err);
}
let err = NixInstallerError::Action(err);
@ -175,7 +175,7 @@ impl InstallPlan {
}
}
write_receipt(self.clone()).await?;
self.write_receipt().await?;
if let Err(err) = crate::self_test::self_test()
.await
@ -283,7 +283,7 @@ impl InstallPlan {
if cancel_channel.try_recv()
!= Err(tokio::sync::broadcast::error::TryRecvError::Empty)
{
if let Err(err) = write_receipt(self.clone()).await {
if let Err(err) = self.write_receipt().await {
tracing::error!("Error saving receipt: {:?}", err);
}
@ -319,19 +319,31 @@ impl InstallPlan {
})
}
}
}
async fn write_receipt(plan: InstallPlan) -> Result<(), NixInstallerError> {
pub(crate) async fn write_receipt(&self) -> Result<(), NixInstallerError> {
let install_receipt_path = PathBuf::from(RECEIPT_LOCATION);
let install_receipt_path_tmp = {
let mut install_receipt_path_tmp = install_receipt_path.clone();
install_receipt_path_tmp.set_extension("tmp");
install_receipt_path_tmp
};
let self_json =
serde_json::to_string_pretty(&self).map_err(NixInstallerError::SerializingReceipt)?;
tokio::fs::create_dir_all("/nix")
.await
.map_err(|e| NixInstallerError::RecordingReceipt(PathBuf::from("/nix"), e))?;
let install_receipt_path = PathBuf::from(RECEIPT_LOCATION);
let self_json =
serde_json::to_string_pretty(&plan).map_err(NixInstallerError::SerializingReceipt)?;
tokio::fs::write(&install_receipt_path, format!("{self_json}\n"))
tokio::fs::write(&install_receipt_path_tmp, format!("{self_json}\n"))
.await
.map_err(|e| NixInstallerError::RecordingReceipt(install_receipt_path, e))?;
Result::<(), NixInstallerError>::Ok(())
.map_err(|e| {
NixInstallerError::RecordingReceipt(install_receipt_path_tmp.clone(), e)
})?;
tokio::fs::rename(&install_receipt_path_tmp, &install_receipt_path)
.await
.map_err(|e| NixInstallerError::RecordingReceipt(install_receipt_path.clone(), e))?;
Ok(())
}
}
pub fn current_version() -> Result<Version, NixInstallerError> {

View file

@ -220,7 +220,7 @@ pub struct CommonSettings {
pub enable_flakes: bool,
}
fn default_nix_build_user_id_base() -> u32 {
pub(crate) fn default_nix_build_user_id_base() -> u32 {
use target_lexicon::OperatingSystem;
match OperatingSystem::host() {
@ -229,7 +229,7 @@ fn default_nix_build_user_id_base() -> u32 {
}
}
fn default_nix_build_group_id() -> u32 {
pub(crate) fn default_nix_build_group_id() -> u32 {
use target_lexicon::OperatingSystem;
match OperatingSystem::host() {