gitbutlerapp/gitbutler · error

Failed to create symlink

Error message

Failed to create symlink: {e}. Previous installation was restored.

What it means

Creating the `but` symlink failed after the new app was installed; the installer successfully rolled back by removing the new install, renaming the backup into place, and re-pointing the symlink at the restored app. The bail reports the original symlink error and confirms the previous installation is back.

Solutions

  1. Check what occupies ~/.local/bin/but (`file ~/.local/bin/but`) and remove/rename the non-symlink entry, then retry
  2. Fix permissions on ~/.local/bin (`sudo chown -R $(whoami) ~/.local/bin`)
  3. If Homebrew provides `but`, uninstall that formula first (`brew uninstall but`)
  4. Retry the install - the previous installation was restored, so it is safe

Example fix

// before
let _ = fs::remove_file(&but_symlink);
let symlink_result = unix_fs::symlink(&final_target, &but_symlink);
// after (caller-side precheck)
let meta = fs::symlink_metadata(bin_dir.join("but"));
if let Ok(m) = meta {
    if !m.file_type().is_symlink() {
        fs::rename(bin_dir.join("but"), bin_dir.join("but.user-override")).unwrap();
    }
}
Defensive patterns

Strategy: validation

Validate before calling

let link = home.join(".local/bin/but");
match fs::symlink_metadata(&link) {
    Ok(m) if !m.file_type().is_symlink() => {
        bail!("{} exists as a regular file/dir - move or remove it before install", link.display());
    }
    Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
        bail!("cannot inspect {}: {e}", link.display());
    }
    _ => {}
}

Type guard

fn but_symlink_replaceable(path: &Path) -> bool {
    fs::symlink_metadata(path)
        .map(|m| m.file_type().is_symlink())
        .unwrap_or(false)
}

Try / catch

match result {
    Err(e) if e.to_string().contains("Previous installation was restored") => {
        eprintln!("fix ~/.local/bin/but and retry; old install is intact: {e}");
    }
    other => other?,
}

Prevention

When it happens

Trigger: install_app when unix_fs::symlink(&final_target, &but_symlink) fails while a backup exists - e.g. ~/.local/bin/but exists as a regular file or directory (EEXIST/ENOTDIR), or the bin dir is read-only (EPERM).

Common situations: User previously managed `but` with Homebrew or a manually placed binary at ~/.local/bin/but; PATH management tools recreated the file; ~/.local/bin owned by root after an earlier sudo install.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/bd6735b69f5eabac. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-installer/src/install_macos.rs:252

        // Symlink creation failed - rollback to backup
        warn(&format!(
            "Failed to create symlink: {e} - attempting to restore backup"
        ));
        if install_app_backup.exists() {
            if let Err(remove_err) = fs::remove_dir_all(&install_app) {
                bail!(
                    "Failed to create symlink and failed to remove new installation during rollback: {}. Backup at: {}",
                    remove_err,
                    install_app_backup.display()
                );
            }
            fs::rename(&install_app_backup, &install_app)?;

            let restored_target = install_app.join("Contents/MacOS/gitbutler-tauri");
            let _ = fs::remove_file(&but_symlink);
            let _ = unix_fs::symlink(&restored_target, &but_symlink);

            bail!("Failed to create symlink: {e}. Previous installation was restored.");
        } else {
            bail!("Failed to create symlink: {e}. No backup available to restore.");
        }
    }

    if !validate_installed_binary(&but_symlink) {
        // Try to restore backup
        warn("Final installation verification failed - attempting to restore backup");
        if install_app_backup.exists() {
            fs::remove_dir_all(&install_app)?;
            fs::rename(&install_app_backup, &install_app)?;

            let restored_target = install_app.join("Contents/MacOS/gitbutler-tauri");
            let _ = fs::remove_file(&but_symlink);
            unix_fs::symlink(&restored_target, &but_symlink)?;

            if validate_installed_binary(&but_symlink) {
                success("Backup was restored successfully");

View on GitHub (pinned to 58e5313667)