Compare commits

...

5 Commits

Author SHA1 Message Date
Alexey Min d642a42fe9
WIP on root-exec 2021-06-28 04:02:07 +03:00
Alexey Min 0ef4dccb8f
pmb.gui: add support for choosing device kernel variant 2021-06-28 04:02:00 +03:00
Alexey Min 245bccbfcb
pmb.gui: support sudo askpass helper
Provide our own askpass GUI, or autodetect
one from user's env vars.
2021-06-28 04:02:00 +03:00
Alexey Min a263b0d458
pmb.gui: initial implementation
Can run "pmbootstrap gui" and see a window
Can switch branches (properly!)
Can select manufacturer, device
2021-06-28 04:02:00 +03:00
Alexey Min d3080b8ea6
pmb.gui: add pmbootstrap API abstraction layer
This will allow GUI code to stay relatively independent
of pmbootstrap's internals.

Paartially based on unmerged !1738 ("Implementation for
pmb.api") by Martijn Braam
2021-06-28 03:55:45 +03:00
14 changed files with 805 additions and 3 deletions

View File

@ -31,6 +31,9 @@ def main():
# Initialize or require config
if args.action == "init":
return config_init.frontend(args)
elif args.action == "gui":
# "pmbootstrap gui" also does not require initialized config
return frontend.gui(args)
elif not os.path.exists(args.config):
raise RuntimeError("Please specify a config file, or run"
" 'pmbootstrap init' to generate one.")

View File

@ -71,8 +71,15 @@ def root(args, cmd, suffix="native", working_dir="/", output="log",
executables = executables_absolute_path()
cmd_chroot = [executables["chroot"], chroot, "/bin/sh", "-c",
pmb.helpers.run.flat_cmd(cmd, working_dir)]
cmd_sudo = ["sudo", "env", "-i", executables["sh"], "-c",
pmb.helpers.run.flat_cmd(cmd_chroot, env=env_all)]
if "sudo_askpass_program" in args:
cmd_sudo = ["env", f"SUDO_ASKPASS={args.sudo_askpass_program}",
"sudo", "--askpass", "env", "-i", executables["sh"], "-c",
pmb.helpers.run.flat_cmd(cmd_chroot, env=env_all)]
else:
cmd_sudo = ["sudo", "env", "-i", executables["sh"], "-c",
pmb.helpers.run.flat_cmd(cmd_chroot, env=env_all)]
return pmb.helpers.run_core.core(args, msg, cmd_sudo, None, output,
output_return, check, True,
disable_timeout)

99
pmb/gui/__init__.py Normal file
View File

@ -0,0 +1,99 @@
# Copyright 2021 Alexey Minnekhanov
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
import time
_have_pyqt5 = False
_have_pygtk = False
def test_installed_gui_tooklits():
global _have_pyqt5, _have_pygtk
try:
import PyQt5
_have_pyqt5 = True
except ImportError:
pass
try:
import gi
_have_pygtk = True
except ImportError:
pass
def raise_no_gui_toolkits():
raise RuntimeError(
"Can't run GUI: you need to install either PyQt5 or pygobject!\n"
"You can do it using your distribution's package manager.")
def run_gui_qt5(args):
import pmb.gui.qt5
pmb.gui.qt5.start(args)
def run_gui_gtk(args):
# TODO: implement
pass
def run_gui_autoselect(args):
global _have_pyqt5, _have_pygtk
prefer_qt = False
if os.environ["KDE_FULL_SESSION"] == "true" \
or os.environ["XDG_CURRENT_DESKTOP"] == "KDE":
prefer_qt = True
logging.debug("KDE session detected")
# TODO: add test for magic env vars for Gnome(-based) DEs
# Be fair, true random
if not prefer_qt and time.time() % 2 == 0:
prefer_qt = True
if prefer_qt and _have_pyqt5:
run_gui_qt5(args)
return
# if no Qt preference, try gtk first
if _have_pygtk:
run_gui_gtk(args)
return
if _have_pyqt5:
run_gui_qt5(args)
return
# give up
raise_no_gui_toolkits()
def run_gui(args):
test_installed_gui_tooklits()
if not _have_pygtk and not _have_pyqt5:
raise_no_gui_toolkits()
return
force_qt5 = False
force_gtk = False
if args.qt5:
force_qt5 = args.qt5
if args.gtk:
force_gtk = args.gtk
if force_gtk and force_qt5:
raise RuntimeError("You must select only one of qt5, gtk options!")
if force_gtk:
if not _have_pygtk:
raise RuntimeError("Cannot use gtk: pygobject is not installed!")
run_gui_gtk(args)
elif force_qt5:
if not _have_pyqt5:
raise RuntimeError("Cannot use Qt5: PyQt5 is not installed!")
run_gui_qt5(args)
else:
run_gui_autoselect(args)

112
pmb/gui/askpass.py Executable file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
# Copyright 2021 Alexey Minnekhanov
# SPDX-License-Identifier: GPL-3.0-or-later
# man sudo.conf: askpass:
# The fully qualified path to a helper program used to read the user's
# password when no terminal is available. This may be the case when sudo
# is executed from a graphical (as opposed to text-based) application.
# The program specified by askpass should display the argument passed to
# it as the prompt and write the user's password to the standard output.
import os
import sys
from PyQt5.QtCore import PYQT_VERSION, QObject, pyqtSignal, pyqtSlot,\
pyqtProperty, QStringListModel
from PyQt5.QtGui import QIcon
from PyQt5.QtQml import QQmlApplicationEngine, QQmlListProperty,\
qmlRegisterType
from PyQt5.QtQuick import QQuickWindow
from PyQt5.QtWidgets import QApplication
# since 5.11 PyQt uses internal sip module
if PYQT_VERSION >= 0x050B00: # 5.11.0 == 0x5, 0xB, 0x00
from PyQt5 import sip
else:
import sip
PROMPT = ""
PASSWORD = ""
class Askpass(QObject):
"""
This class is instantiated in QML like:
import Pmb 1.0 as Pmb
Pmb.Askpass {
id: askpass
}
Then prompt property is used inother QML components like
text: asskpass.prompt
askpass.set_pass('...')
"""
prompt_changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
@pyqtProperty(str, notify=prompt_changed)
def prompt(self) -> str:
global PROMPT
return PROMPT
@pyqtSlot(str)
def set_pass(self, p: str) -> None:
global PASSWORD
PASSWORD = p
def sudo_askpass(prompt: str):
global PROMPT, PASSWORD
PROMPT = prompt
script_dir = os.path.dirname(os.path.abspath(__file__))
sip.setdestroyonexit(False)
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(Askpass, 'Pmb', 1, 0, 'Askpass')
engine.load(f"{script_dir}/qml/askpass.qml")
if len(engine.rootObjects()) < 1:
print("Failed to load QML!", file=sys.stderr)
return 1
# set window icon
root_objects = engine.rootObjects() # type: list[QObject]
# QML's ApplicationWindow instantiates QQuickWindow
app_window = root_objects[0] # type: QQuickWindow
app_window.setIcon(QIcon(f"{script_dir}/img/pmos-logo.svg"))
engine.quit.connect(app.quit)
app.exec_()
# print('<put-your-password-here>') # for debugging purposes only
if len(PASSWORD) < 1:
sys.exit(1)
print(PASSWORD)
sys.stdout.flush()
sys.exit(0)
def usage(pname: str):
print(f"Usage: {pname} <prompt>")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) > 1:
sudo_askpass(sys.argv[1])
else:
usage(sys.argv[0])

BIN
pmb/gui/img/header.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
pmb/gui/img/header.xcf Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><g transform="translate(100,100) rotate(180) translate(0, 6.698729810778069)"><polygon points="29.7,17.5 25.9,27.8 15.0,26.0 0.0,0.0 65.0,0.0 58.0,8.5 65.0,17.0 29.8,17.0" fill="#090"/><g transform="translate(100,0) rotate(120) "><polygon points="29.7,17.5 25.9,27.8 15.0,26.0 0.0,0.0 65.0,0.0 58.0,8.5 65.0,17.0 29.8,17.0" fill="#090"/></g><g transform="translate(50,86.60254037844386) rotate(240) "><polygon points="29.7,17.5 25.9,27.8 15.0,26.0 0.0,0.0 65.0,0.0 58.0,8.5 65.0,17.0 29.8,17.0" fill="#090"/></g></g></svg>

After

Width:  |  Height:  |  Size: 609 B

186
pmb/gui/pmb_api.py Normal file
View File

@ -0,0 +1,186 @@
# Copyright 2019 Martijn Braam
# Copyright 2021 Alexey Minnekhanov
# SPDX-License-Identifier: GPL-3.0-or-later
# This file serves as some layer of abstraction on top of raw pmbootstrap API.
# Every GUI layer interaction with pmbootstrap should go through this file.
import logging
# typing is available since python 3.5
from typing import List, Optional, Set
import pmb.chroot
import pmb.config
import pmb.config.pmaports
import pmb.helpers.devices
import pmb.helpers.git
import pmb.helpers.ui
import pmb.parse._apkbuild
class DeviceInfo:
def __init__(self):
# copy deviceinfo_attributes from pmb
for attr_name in pmb.config.deviceinfo_attributes:
setattr(self, attr_name, None)
# properties taken from pmb/config/__init__.py
# general
self.name = None
self.manufacturer = None
self.codename = None
self.year = None
self.dtb = None
self.modules_initfs = []
self.arch = None
# device
self.chassis = None
self.keyboard = False
self.external_storage = False
self.screen_width = None
self.screen_height = None
self.dev_touchscreen = None
self.dev_touchscreen_calibration = None
self.append_dtb = None
# bootloader
self.flash_method = "none"
self.boot_filesystem = None
# flash
self.flash_heimdall_partition_kernel = None
self.flash_heimdall_partition_initfs = None
self.flash_heimdall_partition_system = None
self.flash_heimdall_partition_vbmeta = None
self.flash_fastboot_partition_kernel = None
self.flash_fastboot_partition_system = None
self.flash_fastboot_partition_vbmeta = None
self.generate_legacy_uboot_initfs = None
self.kernel_cmdline = None
self.generate_bootimg = None
self.bootimg_qcdt = False
self.bootimg_mtk_mkimage = False
self.bootimg_dtb_second = False
self.flash_offset_base = None
self.flash_offset_kernel = None
self.flash_offset_ramdisk = None
self.flash_offset_second = None
self.flash_offset_tags = None
self.flash_pagesize = None
self.flash_fastboot_max_size = None
self.flash_sparse = False
self.rootfs_image_sector_size = None
self.sd_embed_firmware = None
self.sd_embed_firmware_step_size = None
self.partition_blacklist = []
self.boot_part_start = None
self.root_filesystem = None
self.flash_kernel_on_update = None
# weston (some legacy?)
self.weston_pixman_type = None
# Keymaps
self.keymaps = []
# extra properties that are used by some devices
self.getty = None
self.no_framebuffer = False
self.framebuffer_landscape = False
self.usb_rndis_function = None
self.usb_idVendor = None
self.usb_idProduct = None
self.mesa_driver = None
self.dev_internal_storage = None
self.dev_internal_storage_repartition = None
self.bootimg_blobpack = None
self.bootimg_pxa = None
self.bootimg_append_seandroidenforce = None
self.disable_dhcpd = False
self.swap_size_recommended = None
self.initfs_compression = None
def __repr__(self):
return f'<DeviceInfo {self.codename}>'
def fill_from_pmb_deviceinfo(self, dev: dict) -> None:
for key in dev:
if not hasattr(self, key):
logging.debug(f"{dev['codename']}: unknown deviceinfo key: "
f"{key}")
setattr(self, key, dev[key])
# split some strings into lists
self.modules_initfs = self.modules_initfs.split() \
if type(self.modules_initfs) == str else []
def list_vendors(args) -> Set[str]:
return pmb.helpers.devices.list_vendors(args)
def list_vendor_codenames(args, vendor: str,
unmaintained: Optional[bool] = None) -> List[str]:
return pmb.helpers.devices.list_codenames(args, vendor, unmaintained)
def list_device_kernels(args, codename: str) -> dict:
"""
Get device kernel subpackages
:param args: global program state
:param codename: device codename (for example qemu-amd64)
:return: dict('kernel_subpkgname' => 'description', ...)
"""
return pmb.parse._apkbuild.kernels(args, codename)
def list_deviceinfos(args) -> List[DeviceInfo]:
""" Get a list of all devices with the information contained in the
deviceinfo
:returns: list of DeviceInfo objects for all known devices
:rtype: List[DeviceInfo]
"""
raw = pmb.helpers.devices.list_deviceinfos(args)
result = []
for device in raw:
row = DeviceInfo()
row.fill_from_pmb_deviceinfo(raw[device])
result.append(row)
return list(sorted(result, key=lambda k: k.codename))
def get_channels_config(args) -> dict:
channels_cfg = pmb.helpers.git.parse_channels_cfg(args)
return channels_cfg["channels"]
def switch_to_channel(args, channel: str) -> None:
# always switch to safe device before switching branch
cfg = pmb.config.load(args)
cfg['pmbootstrap']['device'] = 'qemu-amd64'
pmb.config.save(args, cfg)
# zap!
pmb.chroot.zap(args, confirm=False)
# do the switch
pmb.config.pmaports.switch_to_channel_branch(args, channel)
def get_current_channel(args) -> str:
repo_path = pmb.helpers.git.get_path(args, "pmaports")
# Get branch name (if on branch) or current commit
ref = pmb.helpers.git.rev_parse(args, repo_path,
extra_args=["--abbrev-ref"])
if ref == "HEAD":
ref = pmb.helpers.git.rev_parse(args, repo_path)[0:8]
if ref == "master":
return "edge"
return ref
def list_uis(args, codename: str) -> list[tuple[str, str]]:
info = pmb.parse.deviceinfo(args, codename)
ui_list = pmb.helpers.ui.list(args, info["arch"])
return ui_list

68
pmb/gui/qml/askpass.qml Normal file
View File

@ -0,0 +1,68 @@
import QtQuick 2.5
import QtQuick.Controls 2.12
// cutom package, registered in python code
import Pmb 1.0 as Pmb
ApplicationWindow {
id: appWindow
visible: true
width: 400
height: contentCol.height + 20
title: qsTr("pmbootstrap sudo askpass")
Pmb.Askpass {
id: askpass
}
Item {
id: content
anchors.fill: parent
anchors.margins: 10
Column {
id: contentCol
anchors.top: parent.top
anchors.left: parent.left
width: parent.width
spacing: 5
Row {
anchors.horizontalCenter: parent.horizontalCenter
Label {
height: input.height
verticalAlignment: Text.AlignVCenter
text: askpass.prompt !== "" ? askpass.prompt : qsTr("Enter password: ")
}
Item { height: 10; width: 10; } // spacer
TextField {
id: input
echoMode: TextInput.Password
placeholderText: "*****"
onAccepted: returnOk()
}
}
DialogButtonBox {
anchors.horizontalCenter: parent.horizontalCenter
standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
onAccepted: returnOk()
onRejected: {
askpass.set_pass('')
appWindow.close()
}
}
} // Column
}
function returnOk() {
askpass.set_pass(input.text)
appWindow.close()
}
}

130
pmb/gui/qml/main.qml Normal file
View File

@ -0,0 +1,130 @@
import QtQuick 2.5
import QtQuick.Controls 2.12
// cutom package, registered in python code
import Pmb 1.0 as Pmb
ApplicationWindow {
id: appWindow
visible: true
width: 800
height: 534
title: qsTr("pmbootstrap GUI")
header: Item {
height: bgImg.height
Rectangle {
// white background
color: "white"
height: bgImg.height
width: parent.width
}
Image {
id: bgImg
cache: true
source: "../img/header.jpg"
height: 111
width: 1076
}
}
Pmb.Devices {
id: pmbDevices
}
Item {
id: content
anchors.fill: parent
anchors.margins: 10
Column {
anchors.top: parent.top
anchors.left: parent.left
width: parent.width
Row {
height: cbChannels.height
Label {
height: cbChannels.height
verticalAlignment: Text.AlignVCenter
text: qsTr("Select postmarketOS release: ")
}
ComboBox {
id: cbChannels
model: pmbDevices.channels
currentIndex: pmbDevices.current_channel
onActivated: {
pmbDevices.set_channel(index)
}
}
}
Row {
height: cbVendors.height
Label {
height: cbVendors.height
verticalAlignment: Text.AlignVCenter
text: qsTr("Select manufacturer: ")
}
ComboBox {
id: cbVendors
model: pmbDevices.vendors
onActivated: {
pmbDevices.select_vendor(index)
}
}
}
Row {
height: cbDevices.height
Label {
height: cbDevices.height
verticalAlignment: Text.AlignVCenter
text: qsTr("Select device: ")
}
ComboBox {
id: cbDevices
model: pmbDevices.vendor_devices
onActivated: {
pmbDevices.select_device(index)
}
}
}
Row {
height: cbDeviceKernels.height
Label {
height: cbDeviceKernels.height
verticalAlignment: Text.AlignVCenter
text: qsTr("Select kernel: ")
}
ComboBox {
id: cbDeviceKernels
model: pmbDevices.device_kernels
onActivated: {
pmbDevices.select_kernel(index)
}
}
}
Row {
height: cbUis.height
Label {
height: cbUis.height
verticalAlignment: Text.AlignVCenter
text: qsTr("Select UI: ")
}
ComboBox {
id: cbUis
model: pmbDevices.uis_list
}
}
}
}
//Component.onCompleted: {
//console.log("vendors: ", pmbdevices.vendors)
//}
}

170
pmb/gui/qt5.py Normal file
View File

@ -0,0 +1,170 @@
# Copyright 2021 Alexey Minnekhanov
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
import sys
from PyQt5.QtCore import PYQT_VERSION, QObject, pyqtSignal, pyqtSlot,\
pyqtProperty, QStringListModel
from PyQt5.QtGui import QIcon
from PyQt5.QtQml import QQmlApplicationEngine, QQmlListProperty,\
qmlRegisterType
from PyQt5.QtQuick import QQuickWindow
from PyQt5.QtWidgets import QApplication
# since 5.11 PyQt uses internal sip module
if PYQT_VERSION >= 0x050B00: # 5.11.0 == 0x5, 0xB, 0x00
from PyQt5 import sip
else:
import sip
import pmb.gui.pmb_api
_args = None
class PmbDevices(QObject):
vendors_changed = pyqtSignal()
channels_changed = pyqtSignal()
current_channel_changed = pyqtSignal()
devices_changed = pyqtSignal()
device_kernels_changed = pyqtSignal()
uis_changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._vendors = sorted(list(pmb.gui.pmb_api.list_vendors(_args)))
self._channels = pmb.gui.pmb_api.get_channels_config(_args)
self._channel_names = []
for ch in self._channels:
self._channel_names.append(ch)
self._selected_vendor = None
if len(self._vendors) > 0:
self._selected_vendor = 0
self._avail_codenames = []
self._selected_codename = None
self._avail_kernels = None
self._sel_kernel = None
@pyqtProperty(list, notify=vendors_changed)
def vendors(self) -> list[str]:
return self._vendors
@pyqtProperty(list, notify=channels_changed)
def channels(self) -> list[str]:
ret = [ch + " " + self._channels[ch]['description'] for ch in self._channels.keys()]
return ret
@pyqtProperty(int, notify=current_channel_changed)
def current_channel(self) -> int:
cname = pmb.gui.pmb_api.get_current_channel(_args)
if cname in self._channel_names:
return self._channel_names.index(cname)
return -1
@pyqtSlot(int)
def set_channel(self, idx: int) -> None:
pmb.gui.pmb_api.switch_to_channel(_args, self._channel_names[idx])
# reload vendors list
self._vendors = sorted(list(pmb.gui.pmb_api.list_vendors(_args)))
self.vendors_changed.emit()
self._selected_vendor = 0
self.devices_changed.emit()
self._selected_codename = 0
self.uis_changed.emit()
self._avail_kernels = None
self.device_kernels_changed.emit()
@pyqtProperty(list, notify=devices_changed)
def vendor_devices(self) -> list[str]:
if self._selected_vendor is None:
return list()
self._avail_codenames = pmb.gui.pmb_api.list_vendor_codenames(
_args, self._vendors[self._selected_vendor])
return self._avail_codenames
@pyqtSlot(int)
def select_vendor(self, vidx: int) -> None:
self._selected_vendor = vidx
self.devices_changed.emit()
self._selected_codename = None
self.uis_changed.emit()
self._avail_kernels = None
self.device_kernels_changed.emit()
@pyqtSlot(int)
def select_device(self, idx: int) -> None:
self._selected_codename = self._avail_codenames[idx]
self.uis_changed.emit()
self._avail_kernels = None
self.device_kernels_changed.emit()
@pyqtProperty(list, notify=device_kernels_changed)
def device_kernels(self) -> list[str]:
ret = []
if self._selected_codename:
self._avail_kernels = pmb.gui.pmb_api.list_device_kernels(
_args, self._selected_codename)
self._sel_kernel = 0
if self._avail_kernels:
for kernel in self._avail_kernels.keys():
ret.append(f"{kernel} ({self._avail_kernels[kernel]})")
return ret
@pyqtSlot(int)
def select_kernel(self, idx: int) -> None:
self._sel_kernel = idx
@pyqtProperty(list, notify=uis_changed)
def uis_list(self) -> list[str]:
ret = []
if self._selected_codename:
uis = pmb.gui.pmb_api.list_uis(_args, self._selected_codename)
else:
uis = pmb.gui.pmb_api.list_uis(_args, "qemu-amd64")
for tup in uis:
ret.append(f"{tup[0]} ({tup[1]})")
return ret
def configure_sudo_askpass_program(args):
script_dir = os.path.dirname(os.path.abspath(__file__))
# Launch our own GUI askpass handler by default
askpass_program = f"{script_dir}/askpass.py"
if "SSH_ASKPASS" in os.environ:
askpass_program = os.environ["SSH_ASKPASS"]
logging.debug(f"autodetected SSH_ASKPASS program: {askpass_program}")
_args.sudo_askpass_program = askpass_program
def start(args):
global _args
_args = args
configure_sudo_askpass_program(args)
# We will need this, directory which contains this script
script_dir = os.path.dirname(os.path.abspath(__file__))
sip.setdestroyonexit(False)
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
qmlRegisterType(PmbDevices, 'Pmb', 1, 0, 'Devices')
engine.load(f"{script_dir}/qml/main.qml")
if len(engine.rootObjects()) < 1:
logging.error("Failed to load QML!")
return 1
root_objects = engine.rootObjects() # type: list[QObject]
# QML's ApplicationWindow instantiates QQuickWindow
app_window = root_objects[0] # type: QQuickWindow
app_window.setIcon(QIcon(f"{script_dir}/img/pmos-logo.svg"))
engine.quit.connect(app.quit)
retval = app.exec_()
logging.debug(f"Qt5: exiting with code {retval}")
return retval

View File

@ -16,6 +16,7 @@ import pmb.chroot.other
import pmb.config
import pmb.export
import pmb.flasher
import pmb.gui
import pmb.helpers.devices
import pmb.helpers.git
import pmb.helpers.lint
@ -598,3 +599,7 @@ def lint(args):
def status(args):
if not pmb.helpers.status.print_status(args, args.details):
sys.exit(1)
def gui(args):
pmb.gui.run_gui(args)

View File

@ -72,7 +72,18 @@ def root(args, cmd, working_dir=None, output="log", output_return=False,
"""
if env:
cmd = ["sh", "-c", flat_cmd(cmd, env=env)]
cmd = ["sudo"] + cmd
# fom `man sudo`:
# If the -A (askpass) option
# is specified, a (possibly graphical) helper program is executed to
# read the user's password and output the password to the standard output.
# If the SUDO_ASKPASS environment variable is set, it specifies the path
# to the helper program.
if "sudo_askpass_program" in args:
cmd = ["env", f"SUDO_ASKPASS={args.sudo_askpass_program}",
"sudo", "--askpass"] + cmd
else:
cmd = ["sudo"] + cmd
return user(args, cmd, working_dir, output, output_return, check, env,
True)

View File

@ -490,6 +490,15 @@ def arguments_status(subparser):
return ret
def arguments_gui(subparser):
ret = subparser.add_parser("gui", help="Run pmbootstrap with GUI")
ret.add_argument("--qt5", action="store_true",
help="Force Qt-based GUI")
ret.add_argument("--gtk", action="store_true",
help="Force Gtk-based GUI")
return ret
def package_completer(prefix, action, parser, parsed_args):
args = parsed_args
pmb.config.merge_with_args(args)
@ -618,6 +627,7 @@ def arguments():
arguments_newapkbuild(sub)
arguments_lint(sub)
arguments_status(sub)
arguments_gui(sub)
# Action: log
log = sub.add_parser("log", help="follow the pmbootstrap logfile")