首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >一个Python脚本,用于在基于Linux的新设备上安装许多有用的程序和包。

一个Python脚本,用于在基于Linux的新设备上安装许多有用的程序和包。
EN

Code Review用户
提问于 2022-01-03 21:34:35
回答 2查看 73关注 0票数 2

我在基于Linux的设备上工作,无论是在工作中还是在我的个人生活中,都有必要经常擦除这些设备--有时每周多次擦一次。在这种情况下,使用某种脚本来恢复一套有用的软件显然是非常有用的。

在过去的几年里,我一直在使用Shell脚本,但是,由于GitHib决定更加强烈地鼓励使用访问令牌,所以我转而使用Python。设置Git是脚本中最关键的操作之一。其他职责:

  • 根据需要升级Python。
  • 通过APT安装许多有用的软件包。
  • 克隆和安装我自己的存储库。

我将在下面发布脚本核心类的代码。如果你想自己运行脚本,你可以下载完整的回购-它只是几个文件- 这里。但请注意,它将开始尝试安装的东西!

核心课程:

代码语言:javascript
复制
"""
This code defines a class which installs the various packages and repositories
required on this computer.
"""

# Standard imports.
import os
import pathlib
import shutil
import subprocess
import urllib.parse

# Local imports.
from git_credentials import set_up_git_credentials, \
    DEFAULT_PATH_TO_GIT_CREDENTIALS, DEFAULT_PATH_TO_PAT, \
    DEFAULT_USERNAME as DEFAULT_GIT_USERNAME, DEFAULT_EMAIL_ADDRESS

# Local constants.
DEFAULT_OS = "ubuntu"
DEFAULT_TARGET_DIR = str(pathlib.Path.home())
DEFAULT_PATH_TO_WALLPAPER_DIR = \
    os.path.join(DEFAULT_TARGET_DIR, "hmss/wallpaper/")

##############
# MAIN CLASS #
##############

class HMSoftwareInstaller:
    """ The class in question. """
    # Class constants.
    CHROME_DEB = "google-chrome-stable_current_amd64.deb"
    CHROME_STEM = "https://dl.google.com/linux/direct/"
    EXPECTED_PATH_TO_GOOGLE_CHROME_COMMAND = "/usr/bin/google-chrome"
    GIT_URL_STEM = "https://github.com/"
    MISSING_FROM_CHROME = ("eog", "nautilus")
    OTHER_THIRD_PARTY = ("gedit-plugins", "inkscape")
    SUPPORTED_OSS = set(("ubuntu", "chrome-os", "raspian", "linux-based"))
    WALLPAPER_STEM = "wallpaper_t"
    WALLPAPER_EXT = ".png"

    def __init__(
            self,
            this_os=DEFAULT_OS,
            target_dir=DEFAULT_TARGET_DIR,
            thunderbird_num=None,
            path_to_git_credentials=DEFAULT_PATH_TO_GIT_CREDENTIALS,
            path_to_pat=DEFAULT_PATH_TO_PAT,
            git_username=DEFAULT_GIT_USERNAME,
            email_address=DEFAULT_EMAIL_ADDRESS,
            path_to_wallpaper_dir=DEFAULT_PATH_TO_WALLPAPER_DIR
        ):
        self.this_os = this_os
        self.target_dir = target_dir
        self.thunderbird_num = thunderbird_num
        self.path_to_git_credentials = path_to_git_credentials
        self.path_to_pat = path_to_pat
        self.git_username = git_username
        self.email_address = email_address
        self.path_to_wallpaper_dir = path_to_wallpaper_dir
        self.failure_log = []

    def check_os(self):
        """ Test whether the OS we're using is supported. """
        if self.this_os in self.SUPPORTED_OSS:
            return True
        return False

    def move_to_target_dir(self):
        """ Change into the directory where we want to install stuff. """
        os.chdir(self.target_dir)

    def set_up_git(self):
        """ Install Git and set up a personal access token. """
        install_result = install_via_apt("git")
        if not install_result:
            return False
        pat_result = \
            set_up_git_credentials(
                username=self.git_username,
                email_address=self.email_address,
                path_to_git_credentials=self.path_to_git_credentials,
                path_to_pat=self.path_to_pat
            )
        if not pat_result:
            return False
        return True

    def install_google_chrome(self):
        """ Ronseal. """
        if (
            check_command_exists("google-chrome") or
            self.this_os == "chrome-os"
        ):
            return True
        chrome_url = urllib.parse.urljoin(self.CHROME_STEM, self.CHROME_DEB)
        chrome_deb_path = "./"+self.CHROME_DEB
        download_process = subprocess.run(["wget", chrome_url])
        if download_process.returncode != 0:
            return False
        if not install_via_apt(chrome_deb_path):
            return False
        os.remove(chrome_deb_path)
        return True

    def change_wallpaper(self):
        """ Change the wallpaper on the desktop of this computer. """
        if not os.path.exists(self.path_to_wallpaper_dir):
            return False
        if self.thunderbird_num:
            wallpaper_filename = (
                self.WALLPAPER_STEM+
                str(self.thunderbird_num)+
                self.WALLPAPER_EXT
            )
        else:
            wallpaper_filename = "default.jpg"
        wallpaper_path = \
            os.path.join(self.path_to_wallpaper_dir, wallpaper_filename)
        if self.this_os == "ubuntu":
            arguments = [
                "gsettings",
                "set",
                "org.gnome.desktop.background",
                "picture-uri",
                "file:///"+wallpaper_path
            ]
        elif self.this_os == "raspbian":
            arguments = ["pcmanfm", "--set-wallpaper", wallpaper_path]
        else:
            return False
        result = run_with_indulgence(arguments)
        return result

    def make_git_url(self, repo_name):
        """ Make the URL pointing to a given repo. """
        suffix = self.git_username+"/"+repo_name+".git"
        result = urllib.parse.urljoin(self.GIT_URL_STEM, suffix)
        return result

    def install_own_repo(
            self,
            repo_name,
            dependent_packages=None,
            installation_arguments=None
        ):
        """ Install a custom repo. """
        if os.path.exists(repo_name):
            print("Looks like "+repo_name+" already exists...")
            return True
        if dependent_packages:
            for package_name in dependent_packages:
                if not install_via_apt(package_name):
                    return False
        arguments = ["git", "clone", self.make_git_url(repo_name)]
        if not run_with_indulgence(arguments):
            return False
        os.chdir(repo_name)
        if installation_arguments:
            if not run_with_indulgence(arguments):
                os.chdir(self.target_dir)
                return False
        os.chdir(self.target_dir)
        return True

    def install_kingdom_of_cyprus(self):
        """ Install the Kingdom of Cyprus repo. """
        repo_name = "kingdom-of-cyprus"
        dependent_packages = ("sqlite", "sqlitebrowser", "nodejs", "npm")
        result = \
            self.install_own_repo(
                repo_name,
                dependent_packages=dependent_packages
            )
        return result

    def install_chancery(self):
        """ Install the Chancery repos. """
        repo_name = "chancery"
        if not self.install_own_repo(repo_name):
            return False
        repo_name_b = "chancery-b"
        installation_arguments_b = ("sh", "install_3rd_party")
        result = \
            self.install_own_repo(
                repo_name_b,
                installation_arguments=installation_arguments_b
            )
        return result

    def install_hmss(self):
        """ Install the HMSS repo. """
        result = self.install_own_repo("hmss")
        return result

    def install_hgmj(self):
        """ Install the HGMJ repo. """
        repo_name = "hgmj"
        installation_arguments = ("sh", "install_3rd_party")
        result = \
            self.install_own_repo(
                repo_name,
                installation_arguments=installation_arguments
            )
        return result

    def install_other_third_party(self):
        """ Install some other useful packages. """
        result = True
        for package in self.OTHER_THIRD_PARTY:
            if not install_via_apt(package):
                result = False
        if self.this_os == "chrome-os":
            for package in self.MISSING_FROM_CHROME:
                if not install_via_apt(package):
                    result = False
        return result

    def run_essentials(self):
        """ Run those processes which, if they fail, we will have to stop
        the entire program there. """
        print("Checking OS...")
        if not self.check_os():
            self.failure_log.append("Check OS")
            return False
        print("Updating and upgrading...")
        if not update_and_upgrade():
            self.failure_log.append("Update and upgrade")
            return False
        print("Upgrading Python...")
        if not upgrade_python():
            self.failure_log.append("Upgrade Python")
            return False
        print("Setting up Git...")
        if not self.set_up_git():
            self.failure_log.append("Set up Git")
            return False
        return True

    def run_non_essentials(self):
        """ Run the installation processes. """
        result = True
        print("Installing Google Chrome...")
        if not self.install_google_chrome():
            self.failure_log.append("Install Google Chrome")
            result = False
        print("Installing HMSS...")
        if not self.install_hmss():
            self.failure_log.append("Install HMSS")
        print("Installing Kingdom of Cyprus...")
        if not self.install_kingdom_of_cyprus():
            self.failure_log.append("Install Kingdom of Cyprus")
            result = False
        print("Installing Chancery repos...")
        if not self.install_chancery():
            self.failure_log.append("Install Chancery repos")
            result = False
        print("Installing HGMJ...")
        if not self.install_hgmj():
            self.failure_log.append("Install HGMJ")
            result = False
        print("Installing other third party...")
        if not self.install_other_third_party():
            self.failure_log.append("Install other third party")
            result = False
        print("Changing wallpaper...")
        if not self.change_wallpaper():
            self.failure_log.append("Change wallpaper")
            # It doesn't matter too much if this fails.
        return result

    def print_outcome(self, passed, with_flying_colours):
        """ Print a list of what failed to the screen. """
        if passed and with_flying_colours:
            print("Installation PASSED with flying colours!")
            return
        if passed:
            print("Installation PASSED but with non-essential failures.")
        else:
            print("Installation FAILED.")
        print("\nThe following items failed:\n")
        for item in self.failure_log:
            print("    * "+item)
        print(" ")

    def run(self):
        """ Run the software installer. """
        print("Running His Majesty's Software Installer...")
        get_sudo()
        self.move_to_target_dir()
        if not self.run_essentials():
            print("\nFinished.\n\n")
            self.print_outcome(False, False)
            return False
        with_flying_colours = self.run_non_essentials()
        print("\nComplete!\n")
        self.print_outcome(True, with_flying_colours)
        return True

####################
# HELPER FUNCTIONS #
####################

def get_sudo():
    """ Get superuser privileges. """
    print("I'm going to need superuser privileges for this...")
    subprocess.run(
        ["sudo", "echo", "Superuser privileges: activate!"],
        check=True
    )

def run_with_indulgence(arguments, show_output=False):
    """ Run a command, and don't panic immediately if we get a non-zero
    return code. """
    if show_output:
        print("Running subprocess.run() with arguments:")
        print(arguments)
        process = subprocess.run(arguments)
    else:
        process = subprocess.run(arguments, stdout=subprocess.DEVNULL)
    if process.returncode == 0:
        return True
    return False

def run_apt_with_argument(argument):
    """ Run APT with an argument, and tell me how it went. """
    arguments = ["sudo", "apt-get", argument]
    result = run_with_indulgence(arguments)
    return result

def check_against_dpkg(package_name):
    """ Check whether a given package is on the books with DPKG. """
    result = run_with_indulgence(["dpkg", "--status", package_name])
    return result

def check_command_exists(command):
    """ Check whether a given command exists on this computer. """
    if shutil.which(command):
        return True
    return False

def install_via_apt(package_name, command=None):
    """ Attempt to install a package, and tell me how it went. """
    if not command:
        command = package_name
    if check_command_exists(command):
        return True
    arguments = ["sudo", "apt-get", "--yes", "install", package_name]
    result = run_with_indulgence(arguments)
    return result

def update_and_upgrade():
    """ Update and upgrade the existing software. """
    if not run_apt_with_argument("update"):
        return False
    if not run_apt_with_argument("upgrade"):
        return False
    if not install_via_apt("software-properties-common"):
        return False
    return True

def pip3_install(package):
    """ Run `pip3 install [package]`. """
    if run_with_indulgence(["pip3", "install", package]):
        return True
    return False

def upgrade_python():
    """ Install PIP3 and other useful Python hangers-on. """
    result = True
    if not install_via_apt("python3-pip"):
        result = False
    if not pip3_install("pylint"):
        result = False
    if not pip3_install("pytest"):
        result = False
    return result
EN

回答 2

Code Review用户

回答已采纳

发布于 2022-01-03 23:59:33

核选项

有必要经常擦拭这些设备--有时每周多次擦一次

拥有这样的脚本是有用的,因为可重复的环境是有用的;只要您不依赖它来解决其他问题。你的两种情景:

在工作中:我们正在生产嵌入式Linux设备,我们需要确保每一个新的软件版本都能在未被触摸的硬件上正常安装。

这是像这样的安装脚本的一个很好的应用程序。

在家里:我的笔记本电脑是带有Linux的Chromebook;每隔几个月,我就会遇到一个问题,就是删除VM并重新启动会更快。

很好,只要你明白出了什么问题,并能够从中吸取教训,然后才能对你的操作系统进行核武器化。

CHROME_DEB似乎是一个奇怪的选择。你在Ubuntu上,为什么不直接从回购处安装它呢?

您应该能够从EXPECTED_PATH_TO_GOOGLE_CHROME_COMMAND的结果中推断出大部分的which

set(("ubuntu", "chrome-os", "raspian", "linux-based"))应该是{"ubuntu", "chrome-os", "raspian", "linux-based"}

在您的__init__中看到的那种死记硬背的成员初始化可以通过使用@dataclass来消除。

self.failure_log = []意味着您要登录到内存中。这是危险的:如果您的进程崩溃,您不会丢失您的日志吗?考虑使用标准的logging模块并将其记录到文件中。

这是:

代码语言:javascript
复制
        if self.this_os in self.SUPPORTED_OSS:
            return True
        return False

应该只是

代码语言:javascript
复制
return self.this_os in self.SUPPORTED_OSS

避免换行符(如pat_result = \中的换行符),这可能只是

代码语言:javascript
复制
        pat_result = set_up_git_credentials(
                username=self.git_username,
                email_address=self.email_address,
                path_to_git_credentials=self.path_to_git_credentials,
                path_to_pat=self.path_to_pat
            )

这是:

代码语言:javascript
复制
    if not pat_result:
        return False
    return True

可能只是

代码语言:javascript
复制
return bool(pat_result)

这种对subprocess的使用:

代码语言:javascript
复制
    download_process = subprocess.run(["wget", chrome_url])
    if download_process.returncode != 0:
        return False

应该用subprocess.check_call代替。这将引发异常,而不是布尔故障指示符--您也应该这样做。对失败状态使用异常而不是布尔返回。您的“基本故障”应该由允许解除堆栈的异常来表示,而您的“非必要故障”应该记录在except中。

将对os.path.exists等的调用替换为对pathlib替代品的调用。

你的install_own_repo很好奇。您接受安装repo_name之前所需的依赖包列表。这表明缺乏适当的包装。如果正确编写,您自己的repos应该构建表达自己依赖关系的debs,这样您就可以安装顶级包,并且依赖项将被自动拉入。使用distutils setup.py来表示依赖关系是您自己开发的另一种方式,如果所有的依赖项都是纯Python的话,这更好。

票数 3
EN

Code Review用户

发布于 2022-01-04 12:42:34

,不要重复,

代码语言:javascript
复制
    def run_non_essentials(self):
        """ Run the installation processes. """
        result = True
        print("Installing Google Chrome...")
        if not self.install_google_chrome():
            self.failure_log.append("Install Google Chrome")
            result = False
        print("Installing HMSS...")
        if not self.install_hmss():
            self.failure_log.append("Install HMSS")
        print("Installing Kingdom of Cyprus...")
        if not self.install_kingdom_of_cyprus():
            self.failure_log.append("Install Kingdom of Cyprus")
            result = False
        print("Installing Chancery repos...")
        if not self.install_chancery():
            self.failure_log.append("Install Chancery repos")
            result = False
        print("Installing HGMJ...")
        if not self.install_hgmj():
            self.failure_log.append("Install HGMJ")
            result = False
        print("Installing other third party...")
        if not self.install_other_third_party():
            self.failure_log.append("Install other third party")
            result = False
        print("Changing wallpaper...")
        if not self.change_wallpaper():
            self.failure_log.append("Change wallpaper")
            # It doesn't matter too much if this fails.
        return result

可概括为:

代码语言:javascript
复制
OPTIONAL_PROGRAMS = {
    "Google Chrome": install_google_chrome,
    "HMSS": install_hmss,
    "Kingdom of Cyprus": install_kingdom_of_cyprus,
    ...
}

[...]

class HMSoftwareInstaller:
    [...]
    
    def install(name: str, function: Callable[[], bool]) -> bool:
        print(f"Installing {name}...")
        
        if not (result := function()):
            self.failure_log.append(f"Install {name}")
        
        return result

    def run_non_essentials(self) -> bool:
        """ Run the installation processes. """
        
        return all([self.install(*program) for program in OPTIONAL_PROGRAMS.items()])

这同样适用于run_essentials()

被调用的函数无论如何都不使用self,因此应该将它们重新定义为空闲函数。

考虑使用pathlib.Path

...to管理路径,而不是os.*函数。

在可能的情况下使用内置的

函数upgrade_python()不必要地存储状态。您可以将其改写为:

代码语言:javascript
复制
def upgrade_python():
    """ Install PIP3 and other useful Python hangers-on. """
    return all([
        install_via_apt("python3-pip"),
        pip3_install("pylint"),
        pip3_install("pytest")
    ])
票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/272595

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档