Last active
June 8, 2026 07:41
-
-
Save birgersp/c7d84daca89d68daa9d68a65ec67e0f0 to your computer and use it in GitHub Desktop.
fedora-silverblue-setup.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import argparse | |
| from os import environ, path | |
| import os | |
| import shutil | |
| import subprocess | |
| from typing import Literal, TypeGuard, get_args | |
| FEDORA_VERSION = 44 | |
| RPM_PACKAGES = [ | |
| "akmods", | |
| "rpmdevtools", | |
| "code", | |
| "dbus-tools", | |
| "git", | |
| "insync", | |
| "mosh", | |
| "pipx", | |
| "postgresql", | |
| "yaru-theme", | |
| "zsh", | |
| "https://desktop.docker.com/linux/main/amd64/docker-desktop-x86_64.rpm", | |
| "https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm", | |
| f"https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{FEDORA_VERSION}.noarch.rpm", | |
| f"https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{FEDORA_VERSION}.noarch.rpm", | |
| ] | |
| FLATPAK_APPS = [ | |
| "com.github.git_cola.git-cola", | |
| "org.pgadmin.pgadmin4", | |
| "com.spotify.Client", | |
| "com.discordapp.Discord", | |
| "io.github.Faugus.faugus-launcher", | |
| "org.sqlitebrowser.sqlitebrowser", | |
| "com.valvesoftware.Steam", | |
| "page.tesk.Refine", | |
| "com.mattjakeman.ExtensionManager", | |
| "io.github.kolunmi.Bazaar", | |
| "io.github.pwr_solaar.solaar", | |
| ] | |
| GNOME_EXTENSIONS = [ | |
| "quicksettings-audio-devices-hider@marcinjahn.com", | |
| "appindicatorsupport@rgcjonas.gmail.com", | |
| "clipboard-indicator@tudmotu.com", | |
| "dash-to-panel@jderose9.github.com", | |
| "emoji-copy@felipeftn", | |
| "gnome-extensions-cli install", | |
| "switcher@landau.fi", | |
| "system-monitor@gnome-shell-extensions.gcampax.github.com", | |
| "window-calls@domandoman.xyz", | |
| ] | |
| HOME = os.environ["HOME"] | |
| CHROME_PROFILE_DIRECTORY = "Default" | |
| CHROME_DESKTOP_ENTRY_FILES = [ | |
| "google-chrome.desktop", | |
| "com.google.Chrome.desktop", | |
| ] | |
| # | |
| # | |
| # | |
| # repo files, installation urls, etc | |
| DOCKER_REPO_PATH = "/etc/yum.repos.d/docker-ce.repo" | |
| DOCKER_REPO_URL = "https://download.docker.com/linux/fedora/docker-ce.repo" | |
| VSCODE_REPO_PATH = "/etc/yum.repos.d/vscode.repo" | |
| VSCODE_REPO_FILE = """[vscode-yum] | |
| name=vscode-yum | |
| baseurl=https://packages.microsoft.com/yumrepos/vscode/ | |
| repo_gpgcheck=0 | |
| gpgcheck=0 | |
| enabled=1 | |
| gpgkey=https://packages.microsoft.com/yumrepos/vscode/repodata/repomd.xml.key | |
| """ | |
| NAUTILUS_TYPEAHEAD_REPO_PATH = f"/etc/yum.repos.d/nelsonaloysio-nautilus-typeahead-fedora-{FEDORA_VERSION}.repo" | |
| NAUTILUS_TYPEAHEAD_REPO_URL = f"https://copr.fedorainfracloud.org/coprs/nelsonaloysio/nautilus-typeahead/repo/fedora-44/nelsonaloysio-nautilus-typeahead-fedora-{FEDORA_VERSION}.repo" | |
| INSYNC_REPO_PATH = "/etc/yum.repos.d/insync.repo" | |
| INSYNC_REPO_FILE = """[insync] | |
| name=insync repo | |
| baseurl=http://yum.insync.io/fedora/$releasever/ | |
| gpgcheck=1 | |
| gpgkey=https://d2t3ff60b2tol4.cloudfront.net/repomd.xml.key | |
| enabled=1 | |
| metadata_expire=120m | |
| """ | |
| NVM_INSTALL_URL = "https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh" | |
| # | |
| # | |
| # | |
| # main | |
| ActionKey = Literal["all", "configure-gnome", "configure-chrome", "flatpaks"] | |
| def main() -> None: | |
| alternatives = get_args(ActionKey) | |
| alternativesStr = ", ".join(alternatives) | |
| parser = argparse.ArgumentParser(description="Setup tool",) | |
| parser.add_argument("action", nargs="?", default="all", help=f"command to run (default: all), one of these: {alternativesStr}") | |
| args = parser.parse_args() | |
| action = args.action | |
| if not isAction(action): | |
| print(f"\"{action}\" is an invalid action, use one of these: {alternativesStr}") | |
| exit(1) | |
| action: ActionKey | |
| if action == "all": | |
| doAllActions() | |
| elif action == "configure-gnome": | |
| installGnomeExtensions() | |
| configureGnome() | |
| configureChromeDesktopEntries() | |
| elif action == "configure-chrome": | |
| configureChromeDesktopEntries() | |
| elif action == "flatpaks": | |
| installFlatpaks() | |
| def doAllActions() -> None: | |
| appState = AppState() | |
| if appState.state == "": | |
| appState.setState("1") | |
| if appState.state == "1": | |
| installSystemPackages() | |
| installNautilusTypeahead() | |
| appState.setState("2") | |
| print("you'll need to reboot to proceed. reboot and re-run this script") | |
| exit(0) | |
| if appState.state == "2": | |
| installGnomeExtensions() | |
| configureGnome() | |
| configureChromeDesktopEntries() | |
| setDefaultShellToZsh() | |
| appState.setState("3") | |
| print("you'll need to log out") | |
| exit(0) | |
| # | |
| # | |
| # | |
| # helpers | |
| def assertCommandExists(command: str) -> None: | |
| if not checkCommandExists(command): | |
| print(f"command \"{command}\" not found. perhaps you need to reboot?") | |
| exit(1) | |
| def writeStateFile(fileName: str, content: str) -> None: | |
| stateFilePath = getStateFilePath(fileName) | |
| stateFileDir = os.path.dirname(stateFilePath) | |
| if not os.path.exists(stateFileDir): | |
| os.mkdir(stateFileDir) | |
| with open(stateFilePath, 'w', encoding='utf-8') as file: | |
| file.write(content) | |
| def readStateFile(fileName: str) -> str: | |
| try: | |
| with open(getStateFilePath(fileName)) as file: | |
| return file.read().strip() | |
| except: | |
| return "" | |
| def isAction(value: str) -> TypeGuard[ActionKey]: | |
| return value in get_args(ActionKey) | |
| def optionalBool() -> bool | None: | |
| return None | |
| def executeReturnOutput(command: str) -> str: | |
| print(f"> {command}") | |
| process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) | |
| if process.stdout is None: | |
| raise Exception() | |
| buffer = [] | |
| for line in process.stdout: | |
| buffer.append(line) | |
| process.wait() | |
| output = "".join(buffer).strip() | |
| if process.returncode != 0: | |
| raise subprocess.CalledProcessError(returncode=process.returncode, cmd=command, output=output) | |
| return output | |
| def executePrintOutput(command: str) -> None: | |
| print(f"> {command}") | |
| if subprocess.run(command, shell=True).returncode != 0: | |
| print("Command failed") | |
| exit(1) | |
| def checkInstallVscodeExtension(id: str) -> None: | |
| targetFolder = id.lower().replace("@", "-") | |
| for file in os.listdir(path.join(environ["HOME"], ".vscode", "extensions")): | |
| if file.startswith(targetFolder): | |
| # target folder already present | |
| return | |
| executePrintOutput(f"code --install-extension {id}") | |
| def checkCommandExists(cmd: str) -> bool: | |
| return shutil.which(cmd) != None | |
| def getStateFilePath(fileName: str) -> str: | |
| return path.join(environ["HOME"], ".local", "state", "birgersp-setup", fileName) | |
| def checkSystemIsFedora() -> bool: | |
| return executeReturnOutput("grep '^ID=fedora' /etc/os-release") == "ID=fedora" | |
| def writeFileAsSudo(filePath: str, content: str) -> None: | |
| if not content.endswith("\n"): | |
| content = content + "\n" | |
| subprocess.run(["sudo", "mkdir", "-p", os.path.dirname(filePath)]) | |
| subprocess.run(["sudo", "tee", filePath], input=content, text=True, capture_output=True) | |
| def checkFileExists(filePath: str) -> bool: | |
| return os.path.exists(filePath) | |
| class AppState: | |
| state = "" | |
| def __init__(self): | |
| self.state = readStateFile("state") | |
| def setState(self, newState: str) -> None: | |
| writeStateFile("state", newState) | |
| self.state = newState | |
| # | |
| # | |
| # | |
| # setup action functions | |
| def installSystemPackages() -> None: | |
| print("Installing system packages...") | |
| if not checkFileExists(VSCODE_REPO_PATH): | |
| writeFileAsSudo(VSCODE_REPO_PATH, VSCODE_REPO_FILE) | |
| if not checkFileExists(INSYNC_REPO_PATH): | |
| writeFileAsSudo(INSYNC_REPO_PATH, INSYNC_REPO_FILE) | |
| if not checkFileExists(DOCKER_REPO_PATH): | |
| executePrintOutput(f"sudo wget https://download.docker.com/linux/fedora/docker-ce.repo -O \"{DOCKER_REPO_PATH}\"") | |
| rpmPackagesStr = " ".join(RPM_PACKAGES) | |
| executePrintOutput(f"sudo rpm-ostree install -y {rpmPackagesStr}") | |
| executePrintOutput(f"sudo rpm-ostree override remove nautilus nautilus-extensions papers-nautilus --install nautilus-typeahead") | |
| def installNautilusTypeahead() -> None: | |
| if not checkFileExists(NAUTILUS_TYPEAHEAD_REPO_PATH): | |
| executePrintOutput(f"sudo wget \"{NAUTILUS_TYPEAHEAD_REPO_URL}\" -O \"{NAUTILUS_TYPEAHEAD_REPO_PATH}\"") | |
| executePrintOutput(f"sudo rpm-ostree override remove nautilus nautilus-extensions papers-nautilus --install nautilus-typeahead") | |
| def installFlatpaks() -> None: | |
| flatpaksStr = " ".join(FLATPAK_APPS) | |
| executePrintOutput(f"flatpak install -y flathub {flatpaksStr}") | |
| def installNvm() -> None: | |
| if checkFileExists(path.join(HOME, ".nvm")): | |
| return | |
| executePrintOutput(f"wget -qO- {NVM_INSTALL_URL} | bash") | |
| def installDeno() -> None: | |
| if checkCommandExists("deno"): | |
| return | |
| executePrintOutput("curl -fsSL https://deno.land/install.sh | sh") | |
| def installGnomeExtensions() -> None: | |
| executePrintOutput("pipx install gnome-extensions-cli") | |
| executePrintOutput("pipx ensurepath") | |
| if not checkCommandExists("gnome-extensions-cli"): | |
| print("Command \"install gnome-extensions-cli\" not found. Try re-running in a new shell (where PATH is loaded)") | |
| exit(1) | |
| extensionsStr = " ".join(GNOME_EXTENSIONS) | |
| executePrintOutput(f"gnome-extensions-cli install {extensionsStr}") | |
| def configureGnome() -> None: | |
| commands = [ | |
| "dconf write \"/org/gnome/desktop/interface/icon-theme\" \"'Yaru-blue'\"", | |
| "dconf write \"/org/gnome/desktop/wm/keybindings/switch-applications-backward\" \"@as []\"", | |
| "dconf write \"/org/gnome/desktop/wm/keybindings/switch-applications\" \"@as []\"", | |
| "dconf write \"/org/gnome/desktop/wm/keybindings/switch-windows-backward\" \"['<Shift><Alt>Tab']\"", | |
| "dconf write \"/org/gnome/desktop/wm/keybindings/switch-windows\" \"['<Alt>Tab']\"", | |
| "dconf write \"/org/gnome/desktop/wm/preferences/button-layout\" \"':minimize,maximize,close'\"", | |
| "dconf write \"/org/gnome/desktop/wm/preferences/resize-with-right-button\" \"true\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/binding\" \"'<Control><Alt>t'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/command\" \"'bash /var/home/birger/repo/tools/linux/hotkeys.sh --ctrl-alt-t'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/name\" \"'ctrl+alt+t'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/binding\" \"'<Super>m'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/command\" \"'bash /var/home/birger/repo/tools/linux/hotkeys.sh --super-m'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/name\" \"'super+m'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/binding\" \"'<Super>g'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/command\" \"'bash /var/home/birger/repo/tools/linux/hotkeys.sh --super-g'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/name\" \"'super+g'\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings\" \"['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/', '/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/', '/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom2/']\"", | |
| "dconf write \"/org/gnome/settings-daemon/plugins/media-keys/terminal\" \"@as []\"", | |
| "dconf write \"/org/gnome/shell/extensions/clipboard-indicator/cache-size\" \"20\"", | |
| "dconf write \"/org/gnome/shell/extensions/clipboard-indicator/history-size\" \"100\"", | |
| "dconf write \"/org/gnome/shell/extensions/clipboard-indicator/toggle-menu\" \"['<Control><Alt>h']\"", | |
| "dconf write \"/org/gnome/shell/extensions/dash-to-panel/dot-size\" \"0\"", | |
| "dconf write \"/org/gnome/shell/extensions/dash-to-panel/group-apps\" \"false\"", | |
| "dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-bg-color\" \"'#3d3846'\"", | |
| "dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-panel-opacity\" \"1.0\"", | |
| "dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-use-custom-bg\" \"true\"", | |
| "dconf write \"/org/gnome/shell/extensions/dash-to-panel/trans-use-custom-opacity\" \"false\"", | |
| "dconf write \"/org/gnome/shell/extensions/emoji-copy/always-show\" \"false\"", | |
| "dconf write \"/org/gnome/shell/extensions/emoji-copy/emoji-keybind\" \"['<Ctrl><Alt>period']\"", | |
| "dconf write \"/org/gnome/shell/extensions/switcher/fade-enable\" \"true\"", | |
| "dconf write \"/org/gnome/shell/extensions/switcher/font-size\" \"uint32 24\"", | |
| "dconf write \"/org/gnome/shell/extensions/switcher/max-width-percentage\" \"uint32 50\"", | |
| "dconf write \"/org/gnome/shell/extensions/system-monitor/show-cpu\" \"false\"", | |
| "dconf write \"/org/gnome/shell/extensions/system-monitor/show-download\" \"false\"", | |
| "dconf write \"/org/gnome/shell/extensions/system-monitor/show-swap\" \"false\"", | |
| "dconf write \"/org/gnome/shell/extensions/system-monitor/show-upload\" \"false\"", | |
| "dconf write \"/org/gnome/shell/keybindings/toggle-message-tray\" \"@as []\"", | |
| ] | |
| for command in commands: | |
| executePrintOutput(command) | |
| def configureChromeDesktopEntries() -> None: | |
| applicationsDir = path.join(HOME, ".local", "share", "applications") | |
| os.makedirs(applicationsDir, exist_ok=True) | |
| for desktopEntryFile in CHROME_DESKTOP_ENTRY_FILES: | |
| srcPath = path.join("/usr/share/applications", desktopEntryFile) | |
| destPath = path.join(applicationsDir, desktopEntryFile) | |
| if not checkFileExists(srcPath): | |
| print(f"Skipping {desktopEntryFile}: {srcPath} does not exist") | |
| continue | |
| shutil.copyfile(srcPath, destPath) | |
| with open(destPath, encoding="utf-8") as file: | |
| lines = file.read().splitlines() | |
| replacements = { | |
| "Exec=/usr/bin/google-chrome-stable %U": f"Exec=/usr/bin/google-chrome-stable --profile-directory={CHROME_PROFILE_DIRECTORY} %U", | |
| "Exec=/usr/bin/google-chrome-stable": f"Exec=/usr/bin/google-chrome-stable --profile-directory={CHROME_PROFILE_DIRECTORY}", | |
| "Exec=/usr/bin/google-chrome-stable --incognito": f"Exec=/usr/bin/google-chrome-stable --profile-directory={CHROME_PROFILE_DIRECTORY} --incognito", | |
| } | |
| with open(destPath, "w", encoding="utf-8") as file: | |
| file.write("\n".join([replacements.get(line, line) for line in lines]) + "\n") | |
| if checkCommandExists("update-desktop-database"): | |
| executePrintOutput(f"update-desktop-database \"{applicationsDir}\"") | |
| def setDefaultShellToZsh() -> None: | |
| zshPath = shutil.which("zsh") | |
| if zshPath is None: | |
| print("zsh is not found(?). you might wanna reboot or something") | |
| exit(1) | |
| if os.environ["SHELL"] == zshPath: | |
| # shell is zsh | |
| return | |
| executePrintOutput(f"chsh -s {zshPath}") | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment