Compare commits

..

22 Commits

Author SHA1 Message Date
SteveLauC
96efcc6c0d chore: release v14.0.0 (#652) 2024-01-22 11:13:33 +08:00
SteveLauC
bf72d7bb5a fix: oh-my-zsh step issue #646 (#647) 2024-01-22 09:18:27 +08:00
dependabot[bot]
dadffb1081 chore(deps): bump h2 from 0.3.22 to 0.3.24 (#645)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.22 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.22...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 12:13:19 +08:00
Ned Wolpert
78dc567226 Added an Audit step for FreeBSD and DragonFly packagees. (#640)
* Added an Audit step for FreeBSD and DragonFly.

Allows for auditing the packages to be disabled since they are breaking steps.
Current behaivor is the default, where if the audit fails topgrade stops. Can
be disabled in the [misc] section independenly from other sections
2024-01-08 09:40:01 +08:00
Mike Wood
362ce4f4f9 fix(os) consider Fedora Kinoite and other immutable distros to be the FedoraImmutable (#638)
* fix(os) consider Fedora Kinoite to be the Fedora Silverblue distribution

* fix(os) support additional Fedora immutable variants

Rename FedoraSilverblue Distribution to FedoraImmutable.  Add test cases for Onyx, Sericea and Silverblue.  Rename upgrade method to match distribution.

Fixes #637
2024-01-08 08:48:48 +08:00
Carrol Cox
ab35cd7b10 feat(pipx-update): add quiet flag for pipx upgrade-all on version 1.4.0+ (#635)
This commit introduces conditional logic to the `run_pipx_update` function that checks the installed version of pipx. If the version is 1.4.0 or higher, the `--quiet` argument is added to the `pipx upgrade-all` command to suppress non-critical output during the upgrade process, adhering to the new feature introduced in pipx 1.4.0 as per the documentation (https://pipx.pypa.io/stable/docs/#pipx-upgrade-all). This change aims to make the upgrade process less verbose and more manageable in automated scripts or CI/CD pipelines where log brevity is beneficial.
2023-12-31 11:38:39 +08:00
SteveLauC
15f4ad7cd1 refactor: update pip if extern managed and global.break-system-packages is true (#634)
refactor: update pip if extern managed and global.break-system-packages is true
2023-12-30 18:23:33 +08:00
Rebecca Turner
cbfb92041f Skip nix upgrade-nix when Nix is installed in a nix profile (#622)
Make `nix upgrade-nix` a separate step

Also check that Nix can be upgraded before running `nix upgrade-nix` to
work around a bug.

See: <https://github.com/NixOS/nix/issues/5473>
2023-12-21 08:55:32 +08:00
SteveLauC
a506c67cac fix: remove deprecated brew option '--ignore-pinned' (#629) 2023-12-19 17:09:32 +08:00
SteveLauC
788e0412f6 feat: inform users of breaking changes on first run (#619) 2023-12-03 09:52:35 +08:00
Nils
18b37ce3e3 Update config.example.toml (#621)
Added WinGet setting:
enable_winget = true
2023-11-26 08:06:17 +08:00
Jakob Fels
a15e6748c7 Add option to ignore containers to pull (#613) 2023-11-24 16:44:52 +08:00
SteveLauC
c6d0539fd2 chore(deps): bump all deps (#618) 2023-11-24 07:50:41 +08:00
LeSnake
3eb3867944 Bun packages fixes (#617)
* fix running with --only

* fix error when no packages installed
2023-11-23 06:36:00 +08:00
DomGlusk
810315b0e2 Make zinit and zi use parallel updates (#614)
* Update zsh.rs to make zinit and zi use parallel

* run cargo fmt

---------

Co-authored-by: Dominic Gluskin <rhinoarmyleader@gmail.com>
2023-11-22 11:18:41 +08:00
SteveLauC
b461fc2536 refactor: cleanup for #615 (#616) 2023-11-22 09:34:21 +08:00
Sam Vente
7e63977ba0 revert git pushing functionalities (#615) 2023-11-22 09:04:19 +08:00
SteveLauC
78dec892cf docs: migration and breaking changes (#606) 2023-11-12 11:43:58 +08:00
pacjo
9ea6628b5c docs: fix typo in config.example.toml (#603)
docs(config): fix typo (dfault -> default)
2023-11-10 10:32:15 +08:00
LeSnake
465df2e9be feat: add Bun packages step (#599) 2023-11-05 10:34:21 +08:00
SteveLauC
61ef926849 chore: update issue template label (#596) 2023-11-01 08:57:57 +08:00
SteveLauC
7fa38c593e fix: omz remote execution if ZSH is not present (#592) 2023-10-29 18:05:20 +08:00
27 changed files with 1448 additions and 998 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report
about: Topgrade is misbehaving
title: ''
labels: 'bug'
labels: 'C-bug'
assignees: ''
---

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Can you please support...?
title: ''
labels: ''
labels: 'C-feature request'
assignees: ''
---

12
BREAKINGCHANGES.md Normal file
View File

@@ -0,0 +1,12 @@
1. In 13.0.0, we introduced a new feature, pushing git repos, now this feature
has been removed as some users are not satisfied with it.
For configuration entries, the following ones are gone:
```toml
[git]
pull_only_repos = []
push_only_repos = []
pull_arguments = ""
push_arguments = ""
```

0
BREAKINGCHNAGES_dev.md Normal file
View File

View File

@@ -101,6 +101,19 @@ Be sure to apply your changes to
[`config.example.toml`](https://github.com/topgrade-rs/topgrade/blob/master/config.example.toml),
and have some basic documentations guiding user how to use these options.
## Breaking changes
If your PR introduces a breaking change, document it in `BREAKINGCHANGE_dev.md`,
it should be written in Markdown and wrapped in 80, for example:
```md
1. The configuration location has been updated to x.
2. The step x has been removed.
3. ...
```
## Before you submit your PR
Make sure your patch passes the following tests on your host:

1400
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@ categories = ["os"]
keywords = ["upgrade", "update"]
license = "GPL-3.0"
repository = "https://github.com/topgrade-rs/topgrade"
version = "13.0.0"
version = "14.0.0"
authors = ["Roey Darwish Dror <roey.ghost@gmail.com>", "Thomas Schönauer <t.schoenauer@hgs-wt.at>"]
exclude = ["doc/screenshot.gif"]
exclude = ["doc/screenshot.gif", "BREAKINGCHNAGES_dev.md"]
edition = "2021"
readme = "README.md"
@@ -22,26 +22,26 @@ path = "src/main.rs"
[dependencies]
home = "~0.5"
etcetera = "~0.8"
once_cell = "~1.17"
once_cell = "~1.18"
serde = { version = "~1.0", features = ["derive"] }
toml = "0.5"
toml = "0.8"
which_crate = { version = "~4.1", package = "which" }
shellexpand = "~2.1"
clap = { version = "~3.1", features = ["cargo", "derive"] }
clap_complete = "~3.1"
clap_mangen = "~0.1"
walkdir = "~2.3"
shellexpand = "~3.1"
clap = { version = "~4.4", features = ["cargo", "derive"] }
clap_complete = "~4.4"
clap_mangen = "~0.2"
walkdir = "~2.4"
console = "~0.15"
lazy_static = "~1.4"
chrono = "~0.4"
glob = "~0.3"
strum = { version = "~0.24", features = ["derive"] }
thiserror = "~1.0"
tempfile = "~3.6"
tempfile = "~3.8"
cfg-if = "~1.0"
tokio = { version = "~1.18", features = ["process", "rt-multi-thread"] }
tokio = { version = "~1.34", features = ["process", "rt-multi-thread"] }
futures = "~0.3"
regex = "~1.7"
regex = "~1.10"
semver = "~1.0"
shell-words = "~1.1"
color-eyre = "~0.6"
@@ -49,10 +49,10 @@ tracing = { version = "~0.1", features = ["attributes", "log"] }
tracing-subscriber = { version = "~0.3", features = ["env-filter", "time"] }
merge = "~0.1"
regex-split = "~0.1"
notify-rust = "~4.8"
notify-rust = "~4.10"
[package.metadata.generate-rpm]
assets = [{source = "target/release/topgrade", dest="/usr/bin/topgrade"}]
assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }]
[package.metadata.generate-rpm.requires]
git = "*"
@@ -61,8 +61,7 @@ git = "*"
depends = "$auto,git"
[target.'cfg(unix)'.dependencies]
libc = "~0.2"
nix = "~0.24"
nix = { version = "~0.27", features = ["hostname", "signal", "user"] }
rust-ini = "~0.19"
self_update_crate = { version = "~0.30", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] }

View File

@@ -42,15 +42,18 @@ The compiled binaries contain a self-upgrading feature.
Just run `topgrade`.
Visit the documentation at [topgrade-rs.github.io](https://topgrade-rs.github.io/) for more information.
> **Warning**
> Work in Progress
## Configuration
See `config.example.toml` for an example configuration file.
## Migration and Breaking Changes
Whenever there is a **breaking change**, the major version number will be bumped,
and we will document these changes in the release note, please take a look at
it when updated to a major release.
> Got a question? Feel free to open an issue or discussion!
### Configuration Path
#### `CONFIG_DIR` on each platform

View File

@@ -32,7 +32,7 @@
# Arguments to pass tmux when pulling Repositories
# tmux_arguments = "-S /var/tmux.sock"
# Do not set the terminal title (dfault: true)
# Do not set the terminal title (default: true)
# set_title = true
# Display the time in step titles (default: true)
@@ -155,32 +155,17 @@
[git]
# max_concurrency = 5
# Git repositories that you want to pull and push
# Additional git repositories to pull
# repos = [
# "~/src/*/",
# "~/.config/something"
# ]
# Repositories that you only want to pull
# pull_only_repos = [
# "~/.config/something_else"
# ]
# Repositories that you only want to push
# push_only_repos = [
# "~/src/*/",
# "~/.config/something_third"
# ]
# Don't pull the predefined git repos
# pull_predefined = false
# Arguments to pass Git when pulling repositories
# pull_arguments = "--rebase --autostash"
# Arguments to pass Git when pushing repositories
# push_arguments = "--all"
# Arguments to pass Git when pulling Repositories
# arguments = "--rebase --autostash"
[windows]
# Manually select Windows updates
@@ -197,6 +182,9 @@
# manager such as Scoop or Cargo
# self_rename = true
# Enable WinGet upgrade
# enable_winget = true
[npm]
# Use sudo if the NPM directory isn't owned by the current user
@@ -237,4 +225,6 @@
[distrobox]
# use_root = false
# containers = ["archlinux-latest"]
# containers = ["archlinux-latest"]
[containers]
# ignored_containers = ["ghcr.io/rancher-sandbox/rancher-desktop/rdx-proxy:latest"]

156
src/breaking_changes.rs Normal file
View File

@@ -0,0 +1,156 @@
//! Inform the users of the breaking changes introduced in this major release.
//!
//! Print the breaking changes and possibly a migration guide when:
//! 1. The Topgrade being executed is a new major release
//! 2. This is the first launch of that major release
use crate::terminal::print_separator;
#[cfg(windows)]
use crate::WINDOWS_DIRS;
#[cfg(unix)]
use crate::XDG_DIRS;
use color_eyre::eyre::Result;
use etcetera::base_strategy::BaseStrategy;
use std::{
fs::{read_to_string, OpenOptions},
io::Write,
path::PathBuf,
str::FromStr,
};
/// Version string x.y.z
static VERSION_STR: &str = env!("CARGO_PKG_VERSION");
/// Version info
#[derive(Debug)]
pub(crate) struct Version {
_major: u64,
minor: u64,
patch: u64,
}
impl FromStr for Version {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
const NOT_SEMVER: &str = "Topgrade version is not semantic";
const NOT_NUMBER: &str = "Topgrade version is not dot-separated numbers";
let mut iter = s.split('.').take(3);
let major = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
let minor = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
let patch = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER);
// They cannot be all 0s
assert!(
!(major == 0 && minor == 0 && patch == 0),
"Version numbers can not be all 0s"
);
Ok(Self {
_major: major,
minor,
patch,
})
}
}
impl Version {
/// True if this version is a new major release.
pub(crate) fn is_new_major_release(&self) -> bool {
// We have already checked that they cannot all be zeros, so `self.major`
// is guaranteed to be non-zero.
self.minor == 0 && self.patch == 0
}
}
/// Topgrade's breaking changes
///
/// We store them in the compiled binary.
pub(crate) static BREAKINGCHANGES: &str = include_str!("../BREAKINGCHANGES.md");
/// Return platform's data directory.
fn data_dir() -> PathBuf {
#[cfg(unix)]
return XDG_DIRS.data_dir();
#[cfg(windows)]
return WINDOWS_DIRS.data_dir();
}
/// Return Topgrade's keep file path.
///
/// keep file is a file under the data directory containing a major version
/// number, it will be created on first run and is used to check if an execution
/// of Topgrade is the first run of a major release, for more details, see
/// `first_run_of_major_release()`.
fn keep_file_path() -> PathBuf {
let keep_file = "topgrade_keep";
data_dir().join(keep_file)
}
/// True if this is the first execution of a major release.
pub(crate) fn first_run_of_major_release() -> Result<bool> {
let version = VERSION_STR.parse::<Version>().expect("should be a valid version");
let keep_file = keep_file_path();
// disable this lint here as the current code has better readability
#[allow(clippy::collapsible_if)]
if version.is_new_major_release() {
if !keep_file.exists() || read_to_string(&keep_file)? != VERSION_STR {
return Ok(true);
}
}
Ok(false)
}
/// Print breaking changes to the user.
pub(crate) fn print_breaking_changes() {
let header = format!("Topgrade {VERSION_STR} Breaking Changes");
print_separator(header);
let contents = if BREAKINGCHANGES.is_empty() {
"No Breaking changes"
} else {
BREAKINGCHANGES
};
println!("{contents}\n");
}
/// This function will be ONLY executed when the user has confirmed the breaking
/// changes, once confirmed, we write the keep file, which means the first run
/// of this major release is finished.
pub(crate) fn write_keep_file() -> Result<()> {
std::fs::create_dir_all(data_dir())?;
let keep_file = keep_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(keep_file)?;
let _ = file.write(VERSION_STR.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn is_new_major_release_works() {
let first_major_release: Version = "1.0.0".parse().unwrap();
let under_dev: Version = "0.1.0".parse().unwrap();
assert!(first_major_release.is_new_major_release());
assert!(!under_dev.is_new_major_release());
}
#[test]
#[should_panic(expected = "Version numbers can not be all 0s")]
fn invalid_version() {
let all_0 = "0.0.0";
all_0.parse::<Version>().unwrap();
}
}

View File

@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use clap::{ArgEnum, Parser};
use clap::{Parser, ValueEnum};
use clap_complete::Shell;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
@@ -19,10 +19,10 @@ use serde::Deserialize;
use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
use which_crate::which;
use super::utils::{editor, hostname};
use super::utils::editor;
use crate::command::CommandExt;
use crate::sudo::SudoKind;
use crate::utils::string_prepend_str;
use crate::utils::{hostname, string_prepend_str};
use tracing::{debug, error};
pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml");
@@ -44,7 +44,7 @@ macro_rules! str_value {
pub type Commands = BTreeMap<String, String>;
#[derive(ArgEnum, EnumString, EnumVariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy)]
#[derive(ValueEnum, EnumString, EnumVariantNames, Debug, Clone, PartialEq, Eq, Deserialize, EnumIter, Copy)]
#[clap(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
@@ -53,11 +53,13 @@ pub enum Step {
AppMan,
Asdf,
Atom,
Audit,
Bin,
Bob,
BrewCask,
BrewFormula,
Bun,
BunPackages,
Cargo,
Chezmoi,
Chocolatey,
@@ -158,23 +160,23 @@ pub struct Include {
paths: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Containers {
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
ignored_containers: Option<Vec<String>>,
}
#[derive(Deserialize, Default, Debug, Merge)]
#[serde(deny_unknown_fields)]
pub struct Git {
max_concurrency: Option<usize>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
pull_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
push_arguments: Option<String>,
arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
repos: Option<Vec<String>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
pull_only_repos: Option<Vec<String>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
push_only_repos: Option<Vec<String>>,
pull_predefined: Option<bool>,
}
@@ -417,6 +419,9 @@ pub struct ConfigFile {
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
git: Option<Git>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
containers: Option<Containers>,
#[merge(strategy = crate::utils::merge_strategies::inner_merge_opt)]
windows: Option<Windows>,
@@ -616,22 +621,7 @@ impl ConfigFile {
}
}
if let Some(paths) = result.git.as_mut().and_then(|git| git.pull_only_repos.as_mut()) {
for path in paths.iter_mut() {
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
debug!("Path {} expanded to {}", path, expanded);
*path = expanded;
}
}
if let Some(paths) = result.git.as_mut().and_then(|git| git.push_only_repos.as_mut()) {
for path in paths.iter_mut() {
let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned();
debug!("Path {} expanded to {}", path, expanded);
*path = expanded;
}
}
debug!("Loaded configuration: {:?}", result);
Ok(result)
}
@@ -692,19 +682,19 @@ pub struct CommandLineArgs {
no_retry: bool,
/// Do not perform upgrades for the given steps
#[clap(long = "disable", value_name = "STEP", arg_enum, multiple_values = true)]
#[clap(long = "disable", value_name = "STEP", value_enum, num_args = 1..)]
disable: Vec<Step>,
/// Perform only the specified steps (experimental)
#[clap(long = "only", value_name = "STEP", arg_enum, multiple_values = true)]
#[clap(long = "only", value_name = "STEP", value_enum, num_args = 1..)]
only: Vec<Step>,
/// Run only specific custom commands
#[clap(long = "custom-commands", value_name = "NAME", multiple_values = true)]
#[clap(long = "custom-commands", value_name = "NAME", num_args = 1..)]
custom_commands: Vec<String>,
/// Set environment variables
#[clap(long = "env", value_name = "NAME=VALUE", multiple_values = true)]
#[clap(long = "env", value_name = "NAME=VALUE", num_args = 1..)]
env: Vec<String>,
/// Output debug logs. Alias for `--log-filter debug`.
@@ -724,9 +714,8 @@ pub struct CommandLineArgs {
short = 'y',
long = "yes",
value_name = "STEP",
arg_enum,
multiple_values = true,
min_values = 0
value_enum,
num_args = 0..,
)]
yes: Option<Vec<Step>>,
@@ -753,7 +742,7 @@ pub struct CommandLineArgs {
pub log_filter: String,
/// Print completion script for the given shell and exit
#[clap(long, arg_enum, hide = true)]
#[clap(long, value_enum, hide = true)]
pub gen_completion: Option<Shell>,
/// Print roff manpage and exit
@@ -859,23 +848,17 @@ impl Config {
&self.config_file.commands
}
/// The list of git repositories to push and pull.
/// The list of additional git repositories to pull.
pub fn git_repos(&self) -> Option<&Vec<String>> {
self.config_file.git.as_ref().and_then(|git| git.repos.as_ref())
}
/// The list of additional git repositories to pull.
pub fn git_pull_only_repos(&self) -> Option<&Vec<String>> {
/// The list of docker/podman containers to ignore.
pub fn containers_ignored_tags(&self) -> Option<&Vec<String>> {
self.config_file
.git
.containers
.as_ref()
.and_then(|git| git.pull_only_repos.as_ref())
}
/// The list of git repositories to push.
pub fn git_push_only_repos(&self) -> Option<&Vec<String>> {
self.config_file
.git
.as_ref()
.and_then(|git| git.push_only_repos.as_ref())
.and_then(|containers| containers.ignored_containers.as_ref())
}
/// Tell whether the specified step should run.
@@ -986,19 +969,9 @@ impl Config {
.and_then(|misc| misc.ssh_arguments.as_ref())
}
/// Extra Git arguments for when pushing
pub fn push_git_arguments(&self) -> Option<&String> {
self.config_file
.git
.as_ref()
.and_then(|git| git.push_arguments.as_ref())
}
/// Extra Git arguments for when pulling
pub fn pull_git_arguments(&self) -> Option<&String> {
self.config_file
.git
.as_ref()
.and_then(|git| git.pull_arguments.as_ref())
/// Extra Git arguments
pub fn git_arguments(&self) -> Option<&String> {
self.config_file.git.as_ref().and_then(|git| git.arguments.as_ref())
}
/// Extra Tmux arguments

View File

@@ -1,6 +1,6 @@
//! SIGINT handling in Unix systems.
use crate::ctrlc::interrupted::set_interrupted;
use nix::sys::signal;
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
/// Handle SIGINT. Set the interruption flag.
extern "C" fn handle_sigint(_: i32) {
@@ -10,12 +10,8 @@ extern "C" fn handle_sigint(_: i32) {
/// Set the necessary signal handlers.
/// The function panics on failure.
pub fn set_handler() {
let sig_action = signal::SigAction::new(
signal::SigHandler::Handler(handle_sigint),
signal::SaFlags::empty(),
signal::SigSet::empty(),
);
let sig_action = SigAction::new(SigHandler::Handler(handle_sigint), SaFlags::empty(), SigSet::empty());
unsafe {
signal::sigaction(signal::SIGINT, &sig_action).unwrap();
sigaction(Signal::SIGINT, &sig_action).unwrap();
}
}

View File

@@ -6,19 +6,20 @@ use std::path::PathBuf;
use std::process::exit;
use std::time::Duration;
use crate::breaking_changes::{first_run_of_major_release, print_breaking_changes, write_keep_file};
use clap::CommandFactory;
use clap::{crate_version, Parser};
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use console::Key;
use etcetera::base_strategy::BaseStrategy;
#[cfg(windows)]
use etcetera::base_strategy::Windows;
use etcetera::base_strategy::{BaseStrategy, Xdg};
#[cfg(unix)]
use etcetera::base_strategy::Xdg;
use once_cell::sync::Lazy;
use tracing::debug;
use crate::steps::git::GitAction;
use self::config::{CommandLineArgs, Config, Step};
use self::error::StepFailed;
#[cfg(all(windows, feature = "self-update"))]
@@ -28,6 +29,7 @@ use self::terminal::*;
use self::utils::{install_color_eyre, install_tracing, update_tracing};
mod breaking_changes;
mod command;
mod config;
mod ctrlc;
@@ -45,10 +47,11 @@ mod sudo;
mod terminal;
mod utils;
pub static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
pub static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
pub(crate) static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| home::home_dir().expect("No home directory"));
#[cfg(unix)]
pub(crate) static XDG_DIRS: Lazy<Xdg> = Lazy::new(|| Xdg::new().expect("No home directory"));
#[cfg(windows)]
pub static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
pub(crate) static WINDOWS_DIRS: Lazy<Windows> = Lazy::new(|| Windows::new().expect("No home directory"));
fn run() -> Result<()> {
install_color_eyre()?;
@@ -132,6 +135,18 @@ fn run() -> Result<()> {
let ctx = execution_context::ExecutionContext::new(run_type, sudo, &git, &config);
let mut runner = runner::Runner::new(&ctx);
// If this is the first execution of a major release, inform user of breaking
// changes
if first_run_of_major_release()? {
print_breaking_changes();
if prompt_yesno("Confirmed?")? {
write_keep_file()?;
} else {
exit(1);
}
}
// Self-Update step, this will execute only if:
// 1. the `self-update` feature is enabled
// 2. it is not disabled from configuration (env var/CLI opt/file)
@@ -249,14 +264,14 @@ fn run() -> Result<()> {
runner.execute(Step::Pkg, "DragonFly BSD Packages", || {
dragonfly::upgrade_packages(&ctx)
})?;
dragonfly::audit_packages(&ctx)?;
runner.execute(Step::Audit, "DragonFly Audit", || dragonfly::audit_packages(&ctx))?;
}
#[cfg(target_os = "freebsd")]
{
runner.execute(Step::Pkg, "FreeBSD Packages", || freebsd::upgrade_packages(&ctx))?;
runner.execute(Step::System, "FreeBSD Upgrade", || freebsd::upgrade_freebsd(&ctx))?;
freebsd::audit_packages(&ctx)?;
runner.execute(Step::Audit, "FreeBSD Audit", || freebsd::audit_packages(&ctx))?;
}
#[cfg(target_os = "openbsd")]
@@ -274,11 +289,13 @@ fn run() -> Result<()> {
{
runner.execute(Step::Yadm, "yadm", || unix::run_yadm(&ctx))?;
runner.execute(Step::Nix, "nix", || unix::run_nix(&ctx))?;
runner.execute(Step::Nix, "nix upgrade-nix", || unix::run_nix_self_upgrade(&ctx))?;
runner.execute(Step::Guix, "guix", || unix::run_guix(&ctx))?;
runner.execute(Step::HomeManager, "home-manager", || unix::run_home_manager(&ctx))?;
runner.execute(Step::Asdf, "asdf", || unix::run_asdf(&ctx))?;
runner.execute(Step::Pkgin, "pkgin", || unix::run_pkgin(&ctx))?;
runner.execute(Step::Bun, "bun", || unix::run_bun(&ctx))?;
runner.execute(Step::BunPackages, "bun-packages", || unix::run_bun_packages(&ctx))?;
runner.execute(Step::Shell, "zr", || zsh::run_zr(&ctx))?;
runner.execute(Step::Shell, "antibody", || zsh::run_antibody(&ctx))?;
runner.execute(Step::Shell, "antidote", || zsh::run_antidote(&ctx))?;
@@ -384,35 +401,35 @@ fn run() -> Result<()> {
if config.should_run(Step::Emacs) {
if !emacs.is_doom() {
if let Some(directory) = emacs.directory() {
git_repos.insert_if_repo(directory, GitAction::Pull);
git_repos.insert_if_repo(directory);
}
}
git_repos.insert_if_repo(HOME_DIR.join(".doom.d"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".doom.d"));
}
if config.should_run(Step::Vim) {
git_repos.insert_if_repo(HOME_DIR.join(".vim"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".config/nvim"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".vim"));
git_repos.insert_if_repo(HOME_DIR.join(".config/nvim"));
}
git_repos.insert_if_repo(HOME_DIR.join(".ideavimrc"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".intellimacs"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".ideavimrc"));
git_repos.insert_if_repo(HOME_DIR.join(".intellimacs"));
if config.should_run(Step::Rcm) {
git_repos.insert_if_repo(HOME_DIR.join(".dotfiles"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".dotfiles"));
}
#[cfg(unix)]
{
git_repos.insert_if_repo(zsh::zshrc(), GitAction::Pull);
git_repos.insert_if_repo(zsh::zshrc());
if config.should_run(Step::Tmux) {
git_repos.insert_if_repo(HOME_DIR.join(".tmux"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".tmux"));
}
git_repos.insert_if_repo(HOME_DIR.join(".config/fish"), GitAction::Pull);
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("openbox"), GitAction::Pull);
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("bspwm"), GitAction::Pull);
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("i3"), GitAction::Pull);
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("sway"), GitAction::Pull);
git_repos.insert_if_repo(HOME_DIR.join(".config/fish"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("openbox"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("bspwm"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("i3"));
git_repos.insert_if_repo(XDG_DIRS.config_dir().join("sway"));
}
#[cfg(windows)]
@@ -420,39 +437,24 @@ fn run() -> Result<()> {
WINDOWS_DIRS
.cache_dir()
.join("Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState"),
GitAction::Pull,
);
#[cfg(windows)]
windows::insert_startup_scripts(&mut git_repos).ok();
if let Some(profile) = powershell.profile() {
git_repos.insert_if_repo(profile, GitAction::Pull);
git_repos.insert_if_repo(profile);
}
}
if config.should_run(Step::GitRepos) {
if let Some(custom_git_repos) = config.git_repos() {
for git_repo in custom_git_repos {
git_repos.glob_insert(git_repo, GitAction::Pull);
git_repos.glob_insert(git_repo, GitAction::Push);
git_repos.glob_insert(git_repo);
}
}
if let Some(git_pull_only_repos) = config.git_pull_only_repos() {
for git_repo in git_pull_only_repos {
git_repos.glob_insert(git_repo, GitAction::Pull);
}
}
if let Some(git_push_only_repos) = config.git_push_only_repos() {
for git_repo in git_push_only_repos {
git_repos.glob_insert(git_repo, GitAction::Push);
}
}
runner.execute(Step::GitRepos, "Git repositories", || {
git.multi_repo_step(&git_repos, &ctx)
git.multi_pull_step(&git_repos, &ctx)
})?;
}

View File

@@ -48,7 +48,9 @@ impl Display for Container {
/// Returns a Vector of all containers, with Strings in the format
/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG"
fn list_containers(crt: &Path) -> Result<Vec<Container>> {
///
/// Containers specified in `ignored_containers` will be filtered out.
fn list_containers(crt: &Path, ignored_containers: Option<&Vec<String>>) -> Result<Vec<Container>> {
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
crt.display()
@@ -83,6 +85,16 @@ fn list_containers(crt: &Path) -> Result<Vec<Container>> {
assert_eq!(split_res.len(), 2);
let (repo_tag, image_id) = (split_res[0], split_res[1]);
if let Some(ignored_containers) = ignored_containers {
if ignored_containers
.iter()
.any(|ignored_container| repo_tag.eq(ignored_container))
{
debug!("Skipping ignored container '{}'", line);
continue;
}
}
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
@@ -109,7 +121,8 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
print_separator("Containers");
let mut success = true;
let containers = list_containers(&crt).context("Failed to list Docker containers")?;
let containers =
list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in containers.iter() {

View File

@@ -8,6 +8,7 @@ use std::{fs, io::Write};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use semver::Version;
use tempfile::tempfile_in;
use tracing::{debug, error};
@@ -352,7 +353,20 @@ pub fn run_pipx_update(ctx: &ExecutionContext) -> Result<()> {
let pipx = require("pipx")?;
print_separator("pipx");
ctx.run_type().execute(pipx).arg("upgrade-all").status_checked()
let mut command_args = vec!["upgrade-all"];
// pipx version 1.4.0 introduced a new command argument `pipx upgrade-all --quiet`
// (see https://pipx.pypa.io/stable/docs/#pipx-upgrade-all)
let version_str = Command::new("pipx")
.args(["--version"])
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned());
let version = Version::parse(&version_str?);
if matches!(version, Ok(version) if version >= Version::new(1, 4, 0)) {
command_args.push("--quiet")
}
ctx.run_type().execute(pipx).args(command_args).status_checked()
}
pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> {
@@ -425,20 +439,53 @@ pub fn run_pip3_update(ctx: &ExecutionContext) -> Result<()> {
.output_checked_utf8()
.map_err(|_| SkipStep("pip does not exist".to_string()))?;
let check_externally_managed = "import sysconfig; from os import path; print('Y') if path.isfile(path.join(sysconfig.get_path('stdlib'), 'EXTERNALLY-MANAGED')) else print('N')";
Command::new(&python3)
.args(["-c", check_externally_managed])
let check_extern_managed_script = "import sysconfig; from os import path; print('Y') if path.isfile(path.join(sysconfig.get_path('stdlib'), 'EXTERNALLY-MANAGED')) else print('N')";
let output = Command::new(&python3)
.args(["-c", check_extern_managed_script])
.output_checked_utf8()?;
let stdout = output.stdout.trim();
let extern_managed = match stdout {
"N" => false,
"Y" => true,
_ => unreachable!("unexpected output from `check_extern_managed_script`"),
};
let allow_break_sys_pkg = match Command::new(&python3)
.args(["-m", "pip", "config", "get", "global.break-system-packages"])
.output_checked_utf8()
.map_err(|_| SkipStep("pip may be externally managed".to_string()))
.and_then(|output| match output.stdout.trim() {
"N" => Ok(()),
"Y" => Err(SkipStep("pip is externally managed".to_string())),
_ => {
print_warning("Unexpected output when checking EXTERNALLY-MANAGED");
print_warning(output.stdout.trim());
Err(SkipStep("pip may be externally managed".to_string()))
}
})?;
{
Ok(output) => {
let stdout = output.stdout.trim();
stdout
.parse::<bool>()
.expect("unexpected output that is not `true` or `false`")
}
// it can fail because this key may not be set
//
// ```sh
// $ pip --version
// pip 23.0.1 from /usr/lib/python3/dist-packages/pip (python 3.11)
//
// $ pip config get global.break-system-packages
// ERROR: No such key - global.break-system-packages
//
// $ echo $?
// 1
// ```
Err(_) => false,
};
debug!("pip3 externally managed: {} ", extern_managed);
debug!("pip3 global.break-system-packages: {}", allow_break_sys_pkg);
// Even though pip3 is externally managed, we should still update it if
// `global.break-system-packages` is true.
if extern_managed && !allow_break_sys_pkg {
return Err(SkipStep(
"Skip pip3 update as it is externally managed and global.break-system-packages is not true".to_string(),
)
.into());
}
print_separator("pip3");
if env::var("VIRTUAL_ENV").is_ok() {

View File

@@ -27,17 +27,9 @@ pub struct Git {
git: Option<PathBuf>,
}
#[derive(Clone, Copy)]
pub enum GitAction {
Push,
Pull,
}
#[derive(Debug)]
pub struct Repositories<'a> {
git: &'a Git,
pull_repositories: HashSet<String>,
push_repositories: HashSet<String>,
repositories: HashSet<String>,
glob_match_options: MatchOptions,
bad_patterns: Vec<String>,
}
@@ -52,36 +44,6 @@ fn output_checked_utf8(output: Output) -> Result<()> {
Ok(())
}
}
async fn push_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -> Result<()> {
let path = repo.to_string();
println!("{} {}", style("Pushing").cyan().bold(), path);
let mut command = AsyncCommand::new(git);
command
.stdin(Stdio::null())
.current_dir(&repo)
.args(["push", "--porcelain"]);
if let Some(extra_arguments) = ctx.config().push_git_arguments() {
command.args(extra_arguments.split_whitespace());
}
let output = command.output().await?;
let result = match output.status.success() {
true => Ok(()),
false => Err(format!("Failed to push {repo}")),
};
if result.is_err() {
println!("{} pushing {}", style("Failed").red().bold(), &repo);
};
match result {
Ok(_) => Ok(()),
Err(e) => Err(eyre!(e)),
}
}
async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -> Result<()> {
let path = repo.to_string();
@@ -96,7 +58,7 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -
.current_dir(&repo)
.args(["pull", "--ff-only"]);
if let Some(extra_arguments) = ctx.config().pull_git_arguments() {
if let Some(extra_arguments) = ctx.config().git_arguments() {
command.args(extra_arguments.split_whitespace());
}
@@ -219,7 +181,7 @@ impl Git {
None
}
pub fn multi_repo_step(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
pub fn multi_pull_step(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
// Warn the user about the bad patterns.
//
// NOTE: this should be executed **before** skipping the Git step or the
@@ -230,15 +192,12 @@ impl Git {
.iter()
.for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories")));
if repositories.is_empty() {
return Err(SkipStep(String::from("No repositories to pull or push")).into());
if repositories.repositories.is_empty() {
return Err(SkipStep(String::from("No repositories to pull")).into());
}
print_separator("Git repositories");
self.multi_pull(repositories, ctx)?;
self.multi_push(repositories, ctx)?;
Ok(())
self.multi_pull(repositories, ctx)
}
pub fn multi_pull(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
@@ -246,7 +205,7 @@ impl Git {
if ctx.run_type().dry() {
repositories
.pull_repositories
.repositories
.iter()
.for_each(|repo| println!("Would pull {}", &repo));
@@ -254,7 +213,7 @@ impl Git {
}
let futures_iterator = repositories
.pull_repositories
.repositories
.iter()
.filter(|repo| match has_remotes(git, repo) {
Some(false) => {
@@ -281,47 +240,6 @@ impl Git {
let error = results.into_iter().find(|r| r.is_err());
error.unwrap_or(Ok(()))
}
pub fn multi_push(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> {
let git = self.git.as_ref().unwrap();
if ctx.run_type().dry() {
repositories
.push_repositories
.iter()
.for_each(|repo| println!("Would push {}", &repo));
return Ok(());
}
let futures_iterator = repositories
.push_repositories
.iter()
.filter(|repo| match has_remotes(git, repo) {
Some(false) => {
println!(
"{} {} because it has no remotes",
style("Skipping").yellow().bold(),
repo
);
false
}
_ => true, // repo has remotes or command to check for remotes has failed. proceed to pull anyway.
})
.map(|repo| push_repository(repo.clone(), git, ctx));
let stream_of_futures = if let Some(limit) = ctx.config().git_concurrency_limit() {
iter(futures_iterator).buffer_unordered(limit).boxed()
} else {
futures_iterator.collect::<FuturesUnordered<_>>().boxed()
};
let basic_rt = runtime::Runtime::new()?;
let results = basic_rt.block_on(async { stream_of_futures.collect::<Vec<Result<()>>>().await });
let error = results.into_iter().find(|r| r.is_err());
error.unwrap_or(Ok(()))
}
}
impl<'a> Repositories<'a> {
@@ -334,27 +252,22 @@ impl<'a> Repositories<'a> {
Self {
git,
repositories: HashSet::new(),
bad_patterns: Vec::new(),
glob_match_options,
pull_repositories: HashSet::new(),
push_repositories: HashSet::new(),
}
}
pub fn insert_if_repo<P: AsRef<Path>>(&mut self, path: P, action: GitAction) -> bool {
pub fn insert_if_repo<P: AsRef<Path>>(&mut self, path: P) -> bool {
if let Some(repo) = self.git.get_repo_root(path) {
match action {
GitAction::Push => self.push_repositories.insert(repo),
GitAction::Pull => self.pull_repositories.insert(repo),
};
self.repositories.insert(repo);
true
} else {
false
}
}
pub fn glob_insert(&mut self, pattern: &str, action: GitAction) {
pub fn glob_insert(&mut self, pattern: &str) {
if let Ok(glob) = glob_with(pattern, self.glob_match_options) {
let mut last_git_repo: Option<PathBuf> = None;
for entry in glob {
@@ -370,7 +283,7 @@ impl<'a> Repositories<'a> {
continue;
}
}
if self.insert_if_repo(&path, action) {
if self.insert_if_repo(&path) {
last_git_repo = Some(path);
}
}
@@ -388,27 +301,16 @@ impl<'a> Repositories<'a> {
}
}
/// Return true if `pull_repos` and `push_repos` are both empty.
#[cfg(unix)]
pub fn is_empty(&self) -> bool {
self.pull_repositories.is_empty() && self.push_repositories.is_empty()
self.repositories.is_empty()
}
// The following 2 functions are `#[cfg(unix)]` because they are only used in
// the `oh-my-zsh` step, which is UNIX-only.
#[cfg(unix)]
/// Return true if `pull_repos` is empty.
pub fn pull_is_empty(&self) -> bool {
self.pull_repositories.is_empty()
}
#[cfg(unix)]
/// Remove `path` from `pull_repos`
///
/// # Panic
/// Will panic if `path` is not in the `pull_repos` under a debug build.
pub fn remove_from_pull(&mut self, path: &str) {
let _removed = self.pull_repositories.remove(path);
pub fn remove(&mut self, path: &str) {
let _removed = self.repositories.remove(path);
debug_assert!(_removed);
}
}

View File

@@ -19,7 +19,9 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
println!();
print_separator("DragonFly BSD Audit");
#[allow(clippy::disallowed_methods)]
if !Command::new(sudo)
.args(["/usr/local/sbin/pkg", "audit", "-Fr"])

View File

@@ -30,7 +30,9 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> {
pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> {
let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?;
println!();
print_separator("FreeBSD Audit");
Command::new(sudo)
.args(["/usr/sbin/pkg", "audit", "-Fr"])
.status_checked()?;

View File

@@ -24,7 +24,7 @@ pub enum Distribution {
CentOS,
ClearLinux,
Fedora,
FedoraSilverblue,
FedoraImmutable,
Debian,
Gentoo,
OpenMandriva,
@@ -54,8 +54,12 @@ impl Distribution {
Some("clear-linux-os") => Distribution::ClearLinux,
Some("fedora") | Some("nobara") => {
return if let Some(variant) = variant {
if variant.contains(&"Silverblue") {
Ok(Distribution::FedoraSilverblue)
if variant.contains(&"Silverblue")
|| variant.contains(&"Kinoite")
|| variant.contains(&"Sericea")
|| variant.contains(&"Onyx")
{
Ok(Distribution::FedoraImmutable)
} else {
Ok(Distribution::Fedora)
}
@@ -131,7 +135,7 @@ impl Distribution {
Distribution::Alpine => upgrade_alpine_linux(ctx),
Distribution::Arch => archlinux::upgrade_arch_linux(ctx),
Distribution::CentOS | Distribution::Fedora => upgrade_redhat(ctx),
Distribution::FedoraSilverblue => upgrade_fedora_silverblue(ctx),
Distribution::FedoraImmutable => upgrade_fedora_immutable(ctx),
Distribution::ClearLinux => upgrade_clearlinux(ctx),
Distribution::Debian => upgrade_debian(ctx),
Distribution::Gentoo => upgrade_gentoo(ctx),
@@ -230,7 +234,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> {
Ok(())
}
fn upgrade_fedora_silverblue(ctx: &ExecutionContext) -> Result<()> {
fn upgrade_fedora_immutable(ctx: &ExecutionContext) -> Result<()> {
let ostree = require("rpm-ostree")?;
let mut command = ctx.run_type().execute(ostree);
command.arg("upgrade");
@@ -1036,6 +1040,17 @@ mod tests {
test_template(include_str!("os_release/fedora"), Distribution::Fedora);
}
#[test]
fn test_fedora_immutable() {
test_template(
include_str!("os_release/fedorasilverblue"),
Distribution::FedoraImmutable,
);
test_template(include_str!("os_release/fedorakinoite"), Distribution::FedoraImmutable);
test_template(include_str!("os_release/fedoraonyx"), Distribution::FedoraImmutable);
test_template(include_str!("os_release/fedorasericea"), Distribution::FedoraImmutable);
}
#[test]
fn test_manjaro() {
test_template(include_str!("os_release/manjaro"), Distribution::Arch);

View File

@@ -0,0 +1,23 @@
NAME="Fedora Linux"
VERSION="39.20240105.0 (Kinoite)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39.20240105.0 (Kinoite)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://kinoite.fedoraproject.org"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-kinoite/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://pagure.io/fedora-kde/SIG/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-11-12
VARIANT="Kinoite"
VARIANT_ID=kinoite
OSTREE_VERSION='39.20240105.0'

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39 (Onyx)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39 (Onyx)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/onyx/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-onyx/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="Onyx"
VARIANT_ID=onyx

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39 (Sericea)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39 (Sericea)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/sericea/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-sericea/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://gitlab.com/fedora/sigs/sway/SIG/-/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="Sericea"
VARIANT_ID=sericea

View File

@@ -0,0 +1,22 @@
NAME="Fedora Linux"
VERSION="39 (Silverblue)"
ID=fedora
VERSION_ID=39
VERSION_CODENAME=""
PLATFORM_ID="platform:f39"
PRETTY_NAME="Fedora Linux 39 (Silverblue)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:39"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://silverblue.fedoraproject.org"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-silverblue/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://github.com/fedora-silverblue/issue-tracker/issues"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=39
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=39
SUPPORT_END=2024-05-14
VARIANT="Silverblue"
VARIANT_ID=silverblue

View File

@@ -1,11 +1,15 @@
use std::ffi::OsStr;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::Component;
use std::path::PathBuf;
use std::process::Command;
use std::{env::var, path::Path};
use crate::command::CommandExt;
use crate::{Step, HOME_DIR};
use color_eyre::eyre::eyre;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use home;
use ini::Ini;
@@ -283,7 +287,7 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<
variant.execute(run_type).arg("update").status_checked()?;
variant
.execute(run_type)
.args(["upgrade", "--ignore-pinned", "--formula"])
.args(["upgrade", "--formula"])
.status_checked()?;
if ctx.config().cleanup() {
@@ -365,23 +369,8 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
debug!("nix profile: {:?}", profile_path);
let manifest_json_path = profile_path.join("manifest.json");
// Should we attempt to upgrade Nix with `nix upgrade-nix`?
#[allow(unused_mut)]
let mut should_self_upgrade = cfg!(target_os = "macos");
#[cfg(target_os = "linux")]
{
// We can't use `nix upgrade-nix` on NixOS.
if let Ok(Distribution::NixOS) = Distribution::detect() {
should_self_upgrade = false;
}
}
print_separator("Nix");
let multi_user = fs::metadata(&nix)?.uid() == 0;
debug!("Multi user nix: {}", multi_user);
#[cfg(target_os = "macos")]
{
if require("darwin-rebuild").is_ok() {
@@ -393,30 +382,12 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
}
let run_type = ctx.run_type();
let nix_args = ["--extra-experimental-features", "nix-command"];
if should_self_upgrade {
if multi_user {
ctx.execute_elevated(&nix, true)?
.args(nix_args)
.arg("upgrade-nix")
.status_checked()?;
} else {
run_type
.execute(&nix)
.args(nix_args)
.arg("upgrade-nix")
.status_checked()?;
}
}
run_type.execute(nix_channel).arg("--update").status_checked()?;
if Path::new(&manifest_json_path).exists() {
run_type
.execute(&nix)
.args(nix_args)
.execute(nix)
.args(nix_args())
.arg("profile")
.arg("upgrade")
.arg(".*")
@@ -432,6 +403,123 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> {
}
}
pub fn run_nix_self_upgrade(ctx: &ExecutionContext) -> Result<()> {
let nix = require("nix")?;
// Should we attempt to upgrade Nix with `nix upgrade-nix`?
#[allow(unused_mut)]
let mut should_self_upgrade = cfg!(target_os = "macos");
#[cfg(target_os = "linux")]
{
// We can't use `nix upgrade-nix` on NixOS.
if let Ok(Distribution::NixOS) = Distribution::detect() {
should_self_upgrade = false;
}
}
if !should_self_upgrade {
return Err(SkipStep(String::from(
"`nix upgrade-nix` can only be used on macOS or non-NixOS Linux",
))
.into());
}
if nix_profile_dir(&nix)?.is_none() {
return Err(SkipStep(String::from(
"`nix upgrade-nix` cannot be run when Nix is installed in a profile",
))
.into());
}
print_separator("Nix (self-upgrade)");
let multi_user = fs::metadata(&nix)?.uid() == 0;
debug!("Multi user nix: {}", multi_user);
let nix_args = nix_args();
if multi_user {
ctx.execute_elevated(&nix, true)?
.args(nix_args)
.arg("upgrade-nix")
.status_checked()
} else {
ctx.run_type()
.execute(&nix)
.args(nix_args)
.arg("upgrade-nix")
.status_checked()
}
}
/// If we try to `nix upgrade-nix` but Nix is installed with `nix profile`, we'll get a `does not
/// appear to be part of a Nix profile` error.
///
/// We duplicate some of the `nix` logic here to avoid this.
/// See: <https://github.com/NixOS/nix/blob/f0180487a0e4c0091b46cb1469c44144f5400240/src/nix/upgrade-nix.cc#L102-L139>
///
/// See: <https://github.com/NixOS/nix/issues/5473>
fn nix_profile_dir(nix: &Path) -> Result<Option<PathBuf>> {
// NOTE: `nix` uses the location of the `nix-env` binary for this but we're using the `nix`
// binary; should be the same.
let nix_bin_dir = nix.parent();
if nix_bin_dir.and_then(|p| p.file_name()) != Some(OsStr::new("bin")) {
debug!("Nix is not installed in a `bin` directory: {nix_bin_dir:?}");
return Ok(None);
}
let nix_dir = nix_bin_dir
.and_then(|bin_dir| bin_dir.parent())
.ok_or_else(|| eyre!("Unable to find Nix install directory from Nix binary {nix:?}"))?;
debug!("Found Nix in {nix_dir:?}");
let mut profile_dir = nix_dir.to_path_buf();
while profile_dir.is_symlink() {
profile_dir = profile_dir
.parent()
.ok_or_else(|| eyre!("Path has no parent: {profile_dir:?}"))?
.join(
profile_dir
.read_link()
.wrap_err_with(|| format!("Failed to read symlink {profile_dir:?}"))?,
);
// NOTE: `nix` uses a hand-rolled canonicalize function, Rust just uses `realpath`.
if profile_dir
.canonicalize()
.wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?
.components()
.any(|component| component == Component::Normal(OsStr::new("profiles")))
{
break;
}
}
debug!("Found Nix profile {profile_dir:?}");
let user_env = profile_dir
.canonicalize()
.wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?;
Ok(
if user_env
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with("user-environment"))
.unwrap_or(false)
{
Some(profile_dir)
} else {
None
},
)
}
fn nix_args() -> [&'static str; 2] {
["--extra-experimental-features", "nix-command"]
}
pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {
let yadm = require("yadm")?;
@@ -555,6 +643,19 @@ pub fn run_bun(ctx: &ExecutionContext) -> Result<()> {
ctx.run_type().execute(bun).arg("upgrade").status_checked()
}
pub fn run_bun_packages(ctx: &ExecutionContext) -> Result<()> {
let bun = require("bun")?;
print_separator("Bun Packages");
if !HOME_DIR.join(".bun/install/global/package.json").exists() {
println!("No global packages installed");
return Ok(());
}
ctx.run_type().execute(bun).args(["-g", "update"]).status_checked()
}
/// Update dotfiles with `rcm(7)`.
///
/// See: <https://github.com/thoughtbot/rcm>

View File

@@ -8,7 +8,6 @@ use tracing::debug;
use crate::command::CommandExt;
use crate::execution_context::ExecutionContext;
use crate::steps::git::GitAction;
use crate::terminal::{print_separator, print_warning};
use crate::utils::{require, which};
use crate::{error::SkipStep, steps::git::Repositories};
@@ -240,7 +239,7 @@ pub fn insert_startup_scripts(git_repos: &mut Repositories) -> Result<()> {
if let Ok(lnk) = parselnk::Lnk::try_from(Path::new(&path)) {
debug!("Startup link: {:?}", lnk);
if let Some(path) = lnk.relative_path() {
git_repos.insert_if_repo(&startup_dir.join(path), GitAction::Pull);
git_repos.insert_if_repo(&startup_dir.join(path));
}
}
}

View File

@@ -122,7 +122,10 @@ pub fn run_zinit(ctx: &ExecutionContext) -> Result<()> {
print_separator("zinit");
let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display(),);
let cmd = format!(
"source {} && zinit self-update && zinit update --all -p",
zshrc.display(),
);
ctx.run_type()
.execute(zsh)
.args(["-i", "-c", cmd.as_str()])
@@ -137,7 +140,7 @@ pub fn run_zi(ctx: &ExecutionContext) -> Result<()> {
print_separator("zi");
let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display(),);
let cmd = format!("source {} && zi self-update && zi update --all -p", zshrc.display(),);
ctx.run_type().execute(zsh).args(["-i", "-c", &cmd]).status_checked()
}
@@ -176,21 +179,15 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
// children processes won't get it either, so we source the zshrc and set
// the ZSH variable for topgrade here.
if ctx.under_ssh() {
let zshrc_path = zshrc().require()?;
let output = Command::new("zsh")
.args([
"-c",
// ` > /dev/null` is used in case the user's zshrc will have some stdout output.
format!(
"source {} > /dev/null && export -p | grep ZSH > /dev/null && echo $ZSH",
zshrc_path.display()
)
.as_str(),
])
.output_checked_utf8()?;
let zsh_env = output.stdout.trim();
if !zsh_env.is_empty() {
env::set_var("ZSH", zsh_env);
let res_env_zsh = Command::new("zsh")
.args(["-ic", "print -rn -- ${ZSH:?}"])
.output_checked_utf8();
// this command will fail if `ZSH` is not set
if let Ok(output) = res_env_zsh {
let env_zsh = output.stdout;
debug!("Oh-my-zsh: under SSH, setting ZSH={}", env_zsh);
env::set_var("ZSH", env_zsh);
}
}
@@ -227,11 +224,11 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> {
for entry in WalkDir::new(custom_dir).max_depth(2) {
let entry = entry?;
custom_repos.insert_if_repo(entry.path(), crate::steps::git::GitAction::Pull);
custom_repos.insert_if_repo(entry.path());
}
custom_repos.remove_from_pull(&oh_my_zsh.to_string_lossy());
if !custom_repos.pull_is_empty() {
custom_repos.remove(&oh_my_zsh.to_string_lossy());
if !custom_repos.is_empty() {
println!("Pulling custom plugins and themes");
ctx.git().multi_pull(&custom_repos, ctx)?;
}

View File

@@ -119,44 +119,13 @@ pub fn string_prepend_str(string: &mut String, s: &str) {
*string = new_string;
}
/* sys-info-rs
*
* Copyright (c) 2015 Siyu Wang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#[cfg(target_family = "unix")]
pub fn hostname() -> Result<String> {
use std::ffi;
extern crate libc;
unsafe {
let buf_size = libc::sysconf(libc::_SC_HOST_NAME_MAX) as usize;
let mut buf = Vec::<u8>::with_capacity(buf_size + 1);
if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf_size) < 0 {
return Err(SkipStep(format!("Failed to get hostname: {}", std::io::Error::last_os_error())).into());
}
let hostname_len = libc::strnlen(buf.as_ptr() as *const libc::c_char, buf_size);
buf.set_len(hostname_len);
Ok(ffi::CString::new(buf).unwrap().into_string().unwrap())
match nix::unistd::gethostname() {
Ok(os_str) => Ok(os_str
.into_string()
.map_err(|_| SkipStep("Failed to get a UTF-8 encoded hostname".into()))?),
Err(e) => Err(e.into()),
}
}