Как навсегда добавить путь в PATH пользователя?

Я создаю установщик для одного из моих проектов, который загружает двоичные файлы проекта (в ZIP-файле), а затем распаковывает его в каталог в папке программ ОС, и я хочу добавить этот каталог в PATH .

Мой скрипт представляет собой установщик с пользовательским интерфейсом, и его можно скомпилировать в .exe/исполняемый файл.

Я попробовал это:

import os

os.environ['PATH'] += os.pathsep + install_dir # `install_dir` is the installation directory

Но это не навсегда и не остается в конце сценария.

Это то, что вы обычно настраиваете в оболочке/процессе, который будет выполнять ваш скрипт Python, а не в самом скрипте. В лучшем случае вы можете отредактировать модуль или файл конфигурации, который будет использовать ваш скрипт.

chepner 02.08.2024 22:41

@chepner Можете ли вы объяснить больше?

foxypiratecove37350 02.08.2024 22:45

Например, возможно, файл конфигурации оболочки включает PATH=/some/directory:$PATH, и когда ваш скрипт Python запускается, значение PATH уже содержит каталог, необходимый для любого исполняемого файла, который будет запускаться вашим скриптом. Это тот, кого вы настраиваете не во время выполнения, а во время установки.

chepner 02.08.2024 22:47

@chepner Мой скрипт предназначен для загрузки двоичных файлов моего проекта и добавления каталога, в котором находятся двоичные файлы, в PATH.

foxypiratecove37350 02.08.2024 22:51

Меня беспокоит, что другие программы влияют на конфигурацию моей среды. Сообщите мне, куда загружаются файлы (или, еще лучше, позвольте мне сообщить сценарию, куда их установить после загрузки), и я настрою любую среду, которая должна знать, где находятся двоичные файлы.

chepner 02.08.2024 22:53

Мой скрипт загружает ZIP-файл, содержащий двоичные файлы, во временную папку системы, а затем разархивирует его в C:/Progam Files/MyProject в Windows, /Applications/MyProject в macOS и /usr/local/bin/MyProject в Linux (используя MyProject в качестве имени проекта), а затем удаляет ZIP-файлы из временной папки. .

foxypiratecove37350 02.08.2024 23:03

Ваш скрипт не может повлиять на переменную PATH процесса, который он сам не создает. Лучшее, что он может сделать, — это отредактировать любой файл конфигурации, который используется для установки PATH для данного процесса, а я говорю, что вам не следует этого делать. Пусть тот, кому нужно использовать двоичные файлы, правильно настроит свою среду; вам достаточно сделать их доступными.

chepner 02.08.2024 23:09

Я делаю установщик, чтобы упростить процесс установки двоичных файлов. Это похоже на установщик Python для Windows (только для примера: на самом деле это не одно и то же, но цель та же).

foxypiratecove37350 02.08.2024 23:13

Монтажники устанавливают; они не настраивают другие программы. В идеале тот, кому нужны бинарники, сам решает, куда им идти, а установщик помещает их туда, куда ему сказано. (То есть вы не прописываете /usr/local/bin/MyProject жестко, а позволяете тому, кто запустит ваш установщик, решать, куда MyProject идти.)

chepner 02.08.2024 23:18

Почему вы чувствуете, что вам нужно изменить PATH? В современных ОС с графическим интерфейсом люди запускают программы с помощью значков/ярлыков, которые обычно предоставляют полный путь к exe-файлу. Вероятно, вам нужно будет добавить PATH только в том случае, если это инструмент командной строки. Это?

001 02.08.2024 23:21

@chepner Этот путь является путем установки по умолчанию, я работаю над тем, как его настроить, но прямо сейчас, в этом вопросе, я хочу, как добавить путь установки в системную переменную среды PATH.

foxypiratecove37350 02.08.2024 23:21

@001 Это инструмент CLI.

foxypiratecove37350 02.08.2024 23:22

Вы не знаете. Системы PATH нет, по крайней мере, в UNIX. Существует множество способов, которыми любой конкретный дистрибутив может определить файл конфигурации для процесса 1, из которого любой процесс может унаследовать значение PATH, но обычно PATH изначально определяется более локально, с помощью оболочки входа.

chepner 02.08.2024 23:31

@chepner Под «системой PATH» я просто имел в виду переменную среды.

foxypiratecove37350 02.08.2024 23:35

Я тоже. Нестандартно иметь одно значение для PATH, наследуемое всеми процессами.

chepner 02.08.2024 23:46

Мне нужен тот, который хотя бы использовался для оболочки.

foxypiratecove37350 02.08.2024 23:55

Не существует независимого от платформы способа сделать это. В типичных дистрибутивах Linux вам, вероятно, удастся отредактировать /etc/profile (чего не должен делать ваш установщик), а в macOS вам придется добавить соответствующий файл в /etc/paths.d. В конечном итоге рекомендуется документировать, где ваш установщик будет устанавливать программу, а пользователи этой программы будут следить за тем, чтобы их переменная PATH обновлялась соответствующим образом.

chepner 03.08.2024 00:02

Так как же обстоят дела у других установщиков?

foxypiratecove37350 03.08.2024 00:11

Многие сценарии установки заканчиваются шагом, добавляющим команду к сценарию запуска оболочки пользователя. Или они предоставляют инструмент настройки, который пользователи могут использовать для этого. Или они просто помещают в документацию что-то, что советует пользователям делать это вручную.

Barmar 03.08.2024 06:34

@Barmar Теперь я знаю, посмотрите ответы, которые вы увидите, что я написал скрипт, чтобы добавить путь к PATH (а также удалить их).

foxypiratecove37350 03.08.2024 06:52

Вы не должны изменять свой вопрос так, чтобы он лишал законной силы существующие ответы. Должен ли я удалить свой сейчас?

tripleee 04.08.2024 05:50

@tripleee Извините, извините. Мне очень жаль.

foxypiratecove37350 04.08.2024 17:39
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
22
109
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вот что я сделал (знаю, это не лучшее решение, но оно работает):

import os
import subprocess
import sys

def add_to_path_win(new_path):
    new_path = new_path.strip().replace('/', '\\').removesuffix("\\")

    key = win32api.RegOpenKeyEx(
        win32con.HKEY_CURRENT_USER,
        r"Environment",
        0,
        win32con.KEY_ALL_ACCESS
    )

    current_path, _ = win32api.RegQueryValueEx(key, "Path")

    if new_path in current_path.split(os.pathsep) or \
       new_path + '\\' in current_path.split(os.pathsep):
        return

    new_path_value =  new_path + os.pathsep + current_path

    win32api.RegSetValueEx(key, "Path", 0, win32con.REG_EXPAND_SZ, new_path_value)
    win32api.RegCloseKey(key)

    refresh_path()

def edit_environment_variables_file(file_path, new_path):
    with open(file_path, 'a') as environment_variables_file:
        environment_variables_file.write(f'PATH = "{new_path}{os.pathsep}$PATH"\n')

    refresh_path()

def add_to_path_bash(new_path):
    new_path = new_path.strip().replace('\\', '/').removesuffix('/')

    if os.path.exists(os.path.expanduser('~/.bash_profile')):
        file_path = os.path.expanduser('~/.bash_profile')
    elif os.path.expanduser('~/.profile'):
        file_path = os.path.expanduser('~/.profile')
    else:
        raise FileNotFoundError("Couldn't find the .bash_profile/.profile")

    edit_environment_variables_file(file_path, new_path)

def add_to_path_sh(new_path):
    new_path = new_path.strip().replace('\\', '/').removesuffix('/')

    if os.path.expanduser('~/.profile'):
        file_path = os.path.expanduser('~/.profile')
    else:
        raise FileNotFoundError("Couldn't find the .profile")

    edit_environment_variables_file(file_path, new_path)

def add_to_path_zsh(new_path):
    new_path = new_path.strip().replace('\\', '/').removesuffix('/')

    if os.path.expanduser('~/.zshenv'):
        file_path = os.path.expanduser('~/.zshenv')
    else:
        raise FileNotFoundError("Couldn't find the .zshenv")

    edit_environment_variables_file(file_path, new_path)

def add_to_path_unix(shell):
    if shell == 'bash':
        return add_to_path_bash
    elif shell == 'sh':
        return add_to_path_sh
    elif shell == 'zsh':
        return add_to_path_zsh
    
# ################################################################################

def remove_from_path_win(new_path):
    new_path = new_path.strip().replace('/', '\\').removesuffix("\\")

    key = win32api.RegOpenKeyEx(
        win32con.HKEY_CURRENT_USER,
        r"Environment",
        0,
        win32con.KEY_ALL_ACCESS
    )

    current_path, _ = win32api.RegQueryValueEx(key, "Path")
    current_path = current_path.split(os.pathsep)

    if new_path not in current_path or \
       new_path + '\\' not in current_path:
        return
    
    if new_path + '\\' in current_path:
        new_path = new_path + '\\'
    
    current_path.remove(new_path)

    win32api.RegSetValueEx(key, "Path", 0, win32con.REG_EXPAND_SZ, os.pathsep.join(current_path))
    win32api.RegCloseKey(key)

    refresh_path()

def remove_from_environment_variables_file(file_path, new_path):
    with open(file_path, 'r+') as environment_variables_file:
        content = environment_variables_file.read()
        if f'export PATH = "{new_path}{os.pathsep}$PATH"\n' in content:
            content = content.replace(f'export PATH = "{new_path}{os.pathsep}$PATH"\n', '')
        if f'PATH = "{new_path}{os.pathsep}$PATH"\n' in content:
            content = content.replace(f'PATH = "{new_path}{os.pathsep}$PATH"\n', '')

    refresh_path()

def remove_from_path_bash(new_path):
    new_path = new_path.strip().replace('\\', '/').removesuffix('/')

    if os.path.exists(os.path.expanduser('~/.bash_profile')):
        file_path = os.path.expanduser('~/.bash_profile')
    elif os.path.expanduser('~/.profile'):
        file_path = os.path.expanduser('~/.profile')
    else:
        raise FileNotFoundError("Couldn't find the .bash_profile/.profile")

    remove_from_environment_variables_file(file_path, new_path)

def remove_from_path_sh(new_path):
    new_path = new_path.strip().replace('\\', '/').removesuffix('/')

    if os.path.expanduser('~/.profile'):
        file_path = os.path.expanduser('~/.profile')
    else:
        raise FileNotFoundError("Couldn't find the .profile")

    remove_from_environment_variables_file(file_path, new_path)

def remove_from_path_zsh(new_path):
    new_path = new_path.strip().replace('\\', '/').removesuffix('/')

    if os.path.expanduser('~/.zshenv'):
        file_path = os.path.expanduser('~/.zshenv')
    else:
        raise FileNotFoundError("Couldn't find the .zshenv")

    remove_from_environment_variables_file(file_path, new_path)

def remove_from_path_unix(shell):
    if shell == 'bash':
        return remove_from_path_bash
    elif shell == 'sh':
        return remove_from_path_sh
    elif shell == 'zsh':
        return remove_from_path_zsh

# ################################################################################

def refresh_path_win():
    key = win32api.RegOpenKeyEx(
        win32con.HKEY_CURRENT_USER,
        r"Environment",
        0,
        win32con.KEY_ALL_ACCESS
    )

    current_path, _ = win32api.RegQueryValueEx(key, "Path")

    os.environ['PATH'] = current_path

    win32api.RegCloseKey(key)

def refresh_from_environment_variables_file(shell, file_path):
    proc = subprocess.Popen(
        [shell],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )

    proc.stdin.write(f"source {file_path}\n")
    proc.stdin.write(f"echo $PATH\n")

    path, _ = proc.communicate()

    os.environ['PATH'] = path

def refresh_path_bash():
    if os.path.exists(os.path.expanduser('~/.bash_profile')):
        file_path = '~/.bash_profile'
    elif os.path.expanduser('~/.profile'):
        file_path = '~/.profile'
    else:
        raise FileNotFoundError("Couldn't find the .bash_profile/.profile")

    refresh_from_environment_variables_file(shell, file_path)

def refresh_path_sh():
    if os.path.expanduser('~/.profile'):
        file_path = '~/.profile'
    else:
        raise FileNotFoundError("Couldn't find the .profile")

    refresh_from_environment_variables_file(shell, file_path)

def refresh_path_zsh():
    if os.path.expanduser('~/.zshenv'):
        file_path = '~/.zshenv'
    else:
        raise FileNotFoundError("Couldn't find the .zshenv")

    refresh_from_environment_variables_file(shell, file_path)

def refresh_path_unix(shell):
    if shell == 'bash':
        return refresh_path_bash
    elif shell == 'sh':
        return refresh_path_sh
    elif shell == 'zsh':
        return refresh_path_zsh

if sys.platform in ("win32", 'cygwin'):
    import win32api
    import win32con

    add_to_path = add_to_path_win
    remove_from_path = remove_from_path_win
    refresh_path = refresh_path_win
elif sys.platform.startswith("linux") or sys.platform == "darwin":
    shell = os.environ['SHELL'].removeprefix('/bin/')

    if shell in ('bash', 'sh', 'zsh'):
        add_to_path = add_to_path_unix(shell)
        remove_from_path = remove_from_path_unix(shell)
        refresh_path = refresh_path_unix(shell)
    else:
        raise NotImplementedError(f"Unsupported shell: {shell}")
else:
    raise NotImplementedError(f"Unsupported platform: {sys.platform}")

Я опубликовал это на PyPI в пакете под названием pathenv, исходный код см. https://github.com/foxypiratecove37350/pathenv.

Проблемы:

  • Windows: возможно, некоторые проблемы из-за прав администратора.
  • Linux и macOS: remove_from_path() обрабатывает только два из бесконечно возможных случаев.

Улучшения?

  • Добавьте больше снарядов, чем просто ЗШ, Баш и Ш.

Похоже, вы хотите навязать интерфейс Windows на платформах, где это не будет оценено по достоинству, а иногда даже невозможно.

Изначально вопрос был задан о системе PATH, поэтому здесь есть один раздел, посвященный этому.

Система PATH

Во многих современных Unix-подобных системах система PATH настраивается с помощью файлов /etc.

  • В системах с systemd вы можете добавить файл /etc/environ.d, который обновит систему PATH.
  • На macOS вы можете перетащить файл /etc/paths.d

Однако обычно этого делать не следует. См. раздел «Альтернативы» ниже.

Пользователь PATH

Для этого не существует стандартного механизма; но некоторые установщики, такие как rbenv, добавляют несколько строк в конфигурацию оболочки пользователя с комментарием, указывающим, какой компонент добавил изменение и почему.

Неудивительно, что это имеет тенденцию вызывать ошибки, когда пользователи вручную меняют строки, удаляют, а затем переустанавливают или запускают установщик несколько раз, когда что-то не удалось в первый раз, или они не поняли, что он сделал.

Как и в предыдущем разделе, менее навязчивый подход состоит в том, чтобы предоставить пользователю возможность сначала установить ваше программное обеспечение в разумном месте и просто задокументировать, как он может изменить свои личные PATH при необходимости.

Альтернативы

Системный администратор несет ответственность за поддержание PATH, и вы не хотите саботировать его усилия. Просто установите свои двоичные файлы в /usr/local/bin (или, если вы собираете, например, пакет .deb, в usr/bin в иерархии пакетов) и позвольте пользователю убедиться, что PATH является нормальным.

Если вы настаиваете на создании собственного установщика, убедитесь, что он соответствует Стандарту иерархии файловой системы, который по сути представляет собой то же самое в отношении двоичных файлов, но также содержит подробные инструкции для других файлов (файлы общей конфигурации, файлы данных , библиотеки и т. д.).

Более плодотворный подход — предоставить пакеты для популярных платформ (пакеты Debian будут работать на Ubuntu, Mint и т. д.; RPM для Red Hat, CentOS, Rocky и т. д.; у Alpine и Arch есть свои собственные системы и т. д. и т. п.) и предоставить .tar.gz обычный configure; make; make install как запасной вариант (или, может быть, в вашем случае просто пакет PyPI?)

Менеджеры пакетов обычно устанавливают все в масштабе всей системы и даже не имеют четко определенного понятия «текущий пользователь». Если необходима нетривиальная конфигурация пользователя, возможно, упакуйте отдельный инструмент, который создаст необходимые файлы конфигурации для каждого пользователя, который его вызывает.

На компьютерах Mac, если у вас есть пакет приложений, его следует установить в /Applications; но для чего-то, что не является простым графическим интерфейсом, просто вернитесь к традиционным соглашениям Unix. Для этого не существует стандартной структуры, но большинство пользователей, у которых есть такая необходимость, вероятно, уже используют Homebrew. (Существуют конкурирующие менеджеры пакетов, которые вы также можете поддерживать; но это становится стандартом де-факто.)

Пожалуйста, принесите извинения по двум причинам: - Я уже ответил, но не могу принять его как принятый, потому что Stack Overflow требует, чтобы я подождал 2 дня, чтобы это сделать - И я забыл указать, что отказался от идеи использования системы PATH пользователь PATH.

foxypiratecove37350 03.08.2024 08:30

Кроме того, это установщик с графическим интерфейсом, но мой проект можно установить, загрузив ZIP-файл или скомпилировав его из исходного кода, так что это просто для упрощения установки и использования моего проекта.

foxypiratecove37350 03.08.2024 08:39

Другие вопросы по теме