Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Alexey Min | d642a42fe9 | |
Alexey Min | 0ef4dccb8f | |
Alexey Min | 245bccbfcb | |
Alexey Min | a263b0d458 | |
Alexey Min | d3080b8ea6 |
|
@ -31,6 +31,9 @@ def main():
|
||||||
# Initialize or require config
|
# Initialize or require config
|
||||||
if args.action == "init":
|
if args.action == "init":
|
||||||
return config_init.frontend(args)
|
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):
|
elif not os.path.exists(args.config):
|
||||||
raise RuntimeError("Please specify a config file, or run"
|
raise RuntimeError("Please specify a config file, or run"
|
||||||
" 'pmbootstrap init' to generate one.")
|
" 'pmbootstrap init' to generate one.")
|
||||||
|
|
|
@ -71,8 +71,15 @@ def root(args, cmd, suffix="native", working_dir="/", output="log",
|
||||||
executables = executables_absolute_path()
|
executables = executables_absolute_path()
|
||||||
cmd_chroot = [executables["chroot"], chroot, "/bin/sh", "-c",
|
cmd_chroot = [executables["chroot"], chroot, "/bin/sh", "-c",
|
||||||
pmb.helpers.run.flat_cmd(cmd, working_dir)]
|
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,
|
return pmb.helpers.run_core.core(args, msg, cmd_sudo, None, output,
|
||||||
output_return, check, True,
|
output_return, check, True,
|
||||||
disable_timeout)
|
disable_timeout)
|
||||||
|
|
|
@ -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)
|
|
@ -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])
|
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
@ -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 |
|
@ -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
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
//}
|
||||||
|
}
|
|
@ -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
|
|
@ -16,6 +16,7 @@ import pmb.chroot.other
|
||||||
import pmb.config
|
import pmb.config
|
||||||
import pmb.export
|
import pmb.export
|
||||||
import pmb.flasher
|
import pmb.flasher
|
||||||
|
import pmb.gui
|
||||||
import pmb.helpers.devices
|
import pmb.helpers.devices
|
||||||
import pmb.helpers.git
|
import pmb.helpers.git
|
||||||
import pmb.helpers.lint
|
import pmb.helpers.lint
|
||||||
|
@ -598,3 +599,7 @@ def lint(args):
|
||||||
def status(args):
|
def status(args):
|
||||||
if not pmb.helpers.status.print_status(args, args.details):
|
if not pmb.helpers.status.print_status(args, args.details):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def gui(args):
|
||||||
|
pmb.gui.run_gui(args)
|
||||||
|
|
|
@ -72,7 +72,18 @@ def root(args, cmd, working_dir=None, output="log", output_return=False,
|
||||||
"""
|
"""
|
||||||
if env:
|
if env:
|
||||||
cmd = ["sh", "-c", flat_cmd(cmd, env=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,
|
return user(args, cmd, working_dir, output, output_return, check, env,
|
||||||
True)
|
True)
|
||||||
|
|
|
@ -490,6 +490,15 @@ def arguments_status(subparser):
|
||||||
return ret
|
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):
|
def package_completer(prefix, action, parser, parsed_args):
|
||||||
args = parsed_args
|
args = parsed_args
|
||||||
pmb.config.merge_with_args(args)
|
pmb.config.merge_with_args(args)
|
||||||
|
@ -618,6 +627,7 @@ def arguments():
|
||||||
arguments_newapkbuild(sub)
|
arguments_newapkbuild(sub)
|
||||||
arguments_lint(sub)
|
arguments_lint(sub)
|
||||||
arguments_status(sub)
|
arguments_status(sub)
|
||||||
|
arguments_gui(sub)
|
||||||
|
|
||||||
# Action: log
|
# Action: log
|
||||||
log = sub.add_parser("log", help="follow the pmbootstrap logfile")
|
log = sub.add_parser("log", help="follow the pmbootstrap logfile")
|
||||||
|
|
Loading…
Reference in New Issue