Skip to content

Commit

Permalink
login: only set netrc-file once in Nix config
Browse files Browse the repository at this point in the history
  • Loading branch information
cole-h committed Dec 11, 2023
1 parent dad4c13 commit f8b5f24
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 42 deletions.
37 changes: 35 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ csv = "1.3.0"
handlebars = "4.4.0"
indicatif = { version = "0.17.6", default-features = false }
inquire = "0.6.2"
nix-config-parser = "0.2.0"
nixel = "5.2.0"
once_cell = "1.18.0"
owo-colors = "3.5.0"
Expand Down
245 changes: 205 additions & 40 deletions src/cli/cmd/login/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@ impl LoginSubcommand {
// $XDG_CONFIG_HOME/nix/nix.conf; basically ~/.config/nix/nix.conf
let nix_config_path = xdg.place_config_file("nix/nix.conf")?;
// $XDG_DATA_HOME/fh/netrc; basically ~/.local/share/flakehub/netrc
let netrc_path = xdg.place_data_file("flakehub/netrc")?;
let netrc_file_path = xdg.place_data_file("flakehub/netrc")?;
// $XDG_CONFIG_HOME/fh/auth; basically ~/.config/fh/auth
let token_path = auth_token_path()?;

let nix_config_addition = format!("\nnetrc-file = {}\n", netrc_path.display());
let netrc_file_string = netrc_file_path.display().to_string();
let nix_config_addition = format!("\nnetrc-file = {}\n", netrc_file_string);
let netrc_contents = format!(
"\
machine {frontend_host} login FIXME password {token}\n\
machine {backend_host} login FIXME password {token}\n\
machine {frontend_host} login flakehub password {token}\n\
machine {backend_host} login flakehub password {token}\n\
",
frontend_host = self
.frontend_addr
Expand All @@ -90,45 +91,75 @@ impl LoginSubcommand {
// https://github.com/NixOS/nix/pull/9145 ("WIP: Support access-tokens for fetching tarballs from private sources")
// https://github.com/NixOS/nix/issues/8635 ("Credentials provider support for builtins.fetch*")
// https://github.com/NixOS/nix/issues/8439 ("--access-tokens option does nothing")
tokio::fs::write(netrc_path, &netrc_contents).await?;
tokio::fs::write(token_path, token).await?;

let write_to_nix_conf = crate::cli::cmd::init::prompt::Prompt::bool(&format!(
"May I add `{}` to {}?",
nix_config_addition.trim(),
nix_config_path.display()
));
let write_success = if write_to_nix_conf {
match tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&nix_config_path)
.await
{
Ok(mut file) => {
let write_status = file.write_all(nix_config_addition.as_bytes()).await;
write_status.is_ok()
}
Err(_) => false,
}
tokio::fs::write(&netrc_file_path, &netrc_contents).await?;
tokio::fs::write(&token_path, token).await?;

let nix_config =
nix_config_parser::NixConfig::parse_file(&nix_config_path).unwrap_or_default();
let mut merged_nix_config = nix_config.clone();
let maybe_existing_netrc_file = merged_nix_config
.settings_mut()
.insert("netrc-file".to_string(), netrc_file_string.clone());

let maybe_prompt = match maybe_existing_netrc_file {
// If the setting is the same as we'd set, we don't need to touch the file at all.
Some(existing_netrc_file) if existing_netrc_file == netrc_file_string => None,
// If the settings are different, ask if we can change it.
Some(existing_netrc_file) => Some(format!(
"May I change `netrc-file` from `{}` to `{}`?",
existing_netrc_file, netrc_file_string
)),
// If there is no `netrc-file` setting, ask if we can set it.
None => Some(format!(
"May I set `netrc-file` to `{}`?",
netrc_file_string
)),
};

let maybe_write_to_nix_conf = if let Some(prompt) = maybe_prompt {
Some(crate::cli::cmd::init::prompt::Prompt::bool(&prompt))
} else {
false
None
};

if !write_to_nix_conf {
print!("No problem! ");
}
if !write_success {
print!("Writing to {} failed. ", nix_config_path.display());
}
if !write_success || !write_to_nix_conf {
println!(
"Please add the following contents to {}:\n{nix_config_addition}",
nix_config_path.display()
);
println!(
"Or add the following contents to your existing netrc file:\n\n{netrc_contents}"
);
if let Some(write_to_nix_conf) = maybe_write_to_nix_conf {
let mut write_success = None;
if write_to_nix_conf {
write_success = match tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&nix_config_path)
.await
{
Ok(mut file) => {
let nix_config_contents =
tokio::fs::read_to_string(&nix_config_path).await?;
let nix_config_contents =
merge_nix_configs(nix_config, nix_config_contents, merged_nix_config);
let write_status = file.write_all(nix_config_contents.as_bytes()).await;
Some(write_status.is_ok())
}
Err(_) => Some(false),
};
} else {
print!("No problem! ");
}

let write_failed = write_success.is_some_and(|x| x == false);
if write_failed {
print!("Writing to {} failed. ", nix_config_path.display());
}

if write_failed || !write_to_nix_conf {
println!(
"Please add the following contents to {}:\n{nix_config_addition}",
nix_config_path.display()
);
println!(
"Or add the following contents to your existing netrc file:\n\n\
{netrc_contents}"
);
}
}

if !self.skip_status {
Expand All @@ -146,3 +177,137 @@ pub(crate) fn auth_token_path() -> color_eyre::Result<PathBuf> {

Ok(token_path)
}

// NOTE(cole-h): Adapted from
// https://github.com/DeterminateSystems/nix-installer/blob/0b0172547c4666f6b1eacb6561a59d6b612505a3/src/action/base/create_or_merge_nix_config.rs#L284
const NIX_CONF_COMMENT_CHAR: char = '#';
fn merge_nix_configs(
mut existing_nix_config: nix_config_parser::NixConfig,
mut existing_nix_config_contents: String,
mut merged_nix_config: nix_config_parser::NixConfig,
) -> String {
let mut new_config = String::new();

// We append a newline to ensure that, in the case there are comments at the end of the
// file and _NO_ trailing newline, we still preserve the entire block of comments.
existing_nix_config_contents.push('\n');

let (associated_lines, _, _) = existing_nix_config_contents.split('\n').fold(
(Vec::new(), Vec::new(), false),
|(mut all_assoc, mut current_assoc, mut associating): (
Vec<Vec<String>>,
Vec<String>,
bool,
),
line| {
let line = line.trim();

if line.starts_with(NIX_CONF_COMMENT_CHAR) {
associating = true;
} else if line.is_empty() || !line.starts_with(NIX_CONF_COMMENT_CHAR) {
associating = false;
}

current_assoc.push(line.to_string());

if !associating {
all_assoc.push(current_assoc);
current_assoc = Vec::new();
}

(all_assoc, current_assoc, associating)
},
);

for line_group in associated_lines {
if line_group.is_empty() || line_group.iter().all(|line| line.is_empty()) {
continue;
}

// This expect should never reasonably panic, because we would need a line group
// consisting solely of a comment and nothing else, but unconditionally appending a
// newline to the config string before grouping above prevents this from occurring.
let line_idx = line_group
.iter()
.position(|line| !line.starts_with(NIX_CONF_COMMENT_CHAR))
.expect("There should always be one line without a comment character");

let setting_line = &line_group[line_idx];
let comments = line_group[..line_idx].join("\n");

// If we're here, but the line without a comment char is empty, we have
// standalone comments to preserve, but no settings with inline comments.
if setting_line.is_empty() {
for line in &line_group {
new_config.push_str(line);
new_config.push('\n');
}

continue;
}

// Preserve inline comments for settings we've merged
let to_remove = if let Some((name, value)) = existing_nix_config
.settings()
.iter()
.find(|(name, _value)| setting_line.starts_with(*name))
{
new_config.push_str(&comments);
new_config.push('\n');
new_config.push_str(name);
new_config.push_str(" = ");

if let Some(merged_value) = merged_nix_config.settings_mut().remove(name) {
new_config.push_str(&merged_value);
new_config.push(' ');
} else {
new_config.push_str(value);
}

if let Some(inline_comment_idx) = setting_line.find(NIX_CONF_COMMENT_CHAR) {
let inline_comment = &setting_line[inline_comment_idx..];
new_config.push_str(inline_comment);
new_config.push('\n');
}

Some(name.clone())
} else {
new_config.push_str(&comments);
new_config.push('\n');
new_config.push_str(setting_line);
new_config.push('\n');

None
};

if let Some(to_remove) = to_remove {
existing_nix_config.settings_mut().remove(&to_remove);
}
}

// Add the leftover existing nix config
for (name, value) in existing_nix_config.settings() {
if merged_nix_config.settings().get(name).is_some() {
continue;
}

new_config.push_str(name);
new_config.push_str(" = ");
new_config.push_str(value);
new_config.push('\n');
}

new_config.push('\n');

for (name, value) in merged_nix_config.settings() {
new_config.push_str(name);
new_config.push_str(" = ");
new_config.push_str(value);
new_config.push('\n');
}

new_config
.strip_prefix('\n')
.unwrap_or(&new_config)
.to_owned()
}

0 comments on commit f8b5f24

Please sign in to comment.