The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
19 KiB
561 lines
19 KiB
#!/usr/bin/env python3 |
|
|
|
# Copyright 2013 Jussi Pakkanen |
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
# you may not use this file except in compliance with the License. |
|
# You may obtain a copy of the License at |
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
|
|
# Unless required by applicable law or agreed to in writing, software |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
# See the License for the specific language governing permissions and |
|
# limitations under the License. |
|
|
|
import sys, os, pickle, time, shutil |
|
import build, coredata, environment, optinterpreter |
|
from PyQt5 import uic |
|
from PyQt5.QtWidgets import QApplication, QMainWindow, QHeaderView |
|
from PyQt5.QtWidgets import QComboBox, QCheckBox |
|
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, QVariant, QTimer |
|
import PyQt5.QtCore |
|
import PyQt5.QtWidgets |
|
|
|
priv_dir = os.path.split(os.path.abspath(os.path.realpath(__file__)))[0] |
|
|
|
class PathModel(QAbstractItemModel): |
|
def __init__(self, coredata): |
|
super().__init__() |
|
self.coredata = coredata |
|
self.names = ['Prefix', 'Library dir', 'Binary dir', 'Include dir', 'Data dir',\ |
|
'Man dir', 'Locale dir'] |
|
self.attr_name = ['prefix', 'libdir', 'bindir', 'includedir', 'datadir', \ |
|
'mandir', 'localedir'] |
|
|
|
def flags(self, index): |
|
if index.column() == 1: |
|
editable = PyQt5.QtCore.Qt.ItemIsEditable |
|
else: |
|
editable= 0 |
|
return PyQt5.QtCore.Qt.ItemIsSelectable | PyQt5.QtCore.Qt.ItemIsEnabled | editable |
|
|
|
def rowCount(self, index): |
|
if index.isValid(): |
|
return 0 |
|
return len(self.names) |
|
|
|
def columnCount(self, index): |
|
return 2 |
|
|
|
def headerData(self, section, orientation, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
if section == 1: |
|
return QVariant('Path') |
|
return QVariant('Type') |
|
|
|
def index(self, row, column, parent): |
|
return self.createIndex(row, column) |
|
|
|
def data(self, index, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
row = index.row() |
|
column = index.column() |
|
if column == 0: |
|
return self.names[row] |
|
return getattr(self.coredata, self.attr_name[row]) |
|
|
|
def parent(self, index): |
|
return QModelIndex() |
|
|
|
def setData(self, index, value, role): |
|
if role != PyQt5.QtCore.Qt.EditRole: |
|
return False |
|
row = index.row() |
|
column = index.column() |
|
s = str(value) |
|
setattr(self.coredata, self.attr_name[row], s) |
|
self.dataChanged.emit(self.createIndex(row, column), self.createIndex(row, column)) |
|
return True |
|
|
|
class TargetModel(QAbstractItemModel): |
|
def __init__(self, builddata): |
|
super().__init__() |
|
self.targets = [] |
|
for target in builddata.get_targets().values(): |
|
name = target.get_basename() |
|
num_sources = len(target.get_sources()) + len(target.get_generated_sources()) |
|
if isinstance(target, build.Executable): |
|
typename = 'executable' |
|
elif isinstance(target, build.SharedLibrary): |
|
typename = 'shared library' |
|
elif isinstance(target, build.StaticLibrary): |
|
typename = 'static library' |
|
elif isinstance(target, build.CustomTarget): |
|
typename = 'custom' |
|
else: |
|
typename = 'unknown' |
|
if target.should_install(): |
|
installed = 'Yes' |
|
else: |
|
installed = 'No' |
|
self.targets.append((name, typename, installed, num_sources)) |
|
|
|
def flags(self, index): |
|
return PyQt5.QtCore.Qt.ItemIsSelectable | PyQt5.QtCore.Qt.ItemIsEnabled |
|
|
|
def rowCount(self, index): |
|
if index.isValid(): |
|
return 0 |
|
return len(self.targets) |
|
|
|
def columnCount(self, index): |
|
return 4 |
|
|
|
def headerData(self, section, orientation, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
if section == 3: |
|
return QVariant('Source files') |
|
if section == 2: |
|
return QVariant('Installed') |
|
if section == 1: |
|
return QVariant('Type') |
|
return QVariant('Name') |
|
|
|
def data(self, index, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
row = index.row() |
|
column = index.column() |
|
return self.targets[row][column] |
|
|
|
def index(self, row, column, parent): |
|
return self.createIndex(row, column) |
|
|
|
def parent(self, index): |
|
return QModelIndex() |
|
|
|
class DependencyModel(QAbstractItemModel): |
|
def __init__(self, coredata): |
|
super().__init__() |
|
self.deps = [] |
|
for k in coredata.deps.keys(): |
|
bd = coredata.deps[k] |
|
name = k |
|
found = bd.found() |
|
if found: |
|
cflags = str(bd.get_compile_flags()) |
|
libs = str(bd.get_link_flags()) |
|
found = 'yes' |
|
else: |
|
cflags = '' |
|
libs = '' |
|
found = 'no' |
|
self.deps.append((name, found, cflags, libs)) |
|
|
|
def flags(self, index): |
|
return PyQt5.QtCore.Qt.ItemIsSelectable | PyQt5.QtCore.Qt.ItemIsEnabled |
|
|
|
def rowCount(self, index): |
|
if index.isValid(): |
|
return 0 |
|
return len(self.deps) |
|
|
|
def columnCount(self, index): |
|
return 4 |
|
|
|
def headerData(self, section, orientation, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
if section == 3: |
|
return QVariant('Link flags') |
|
if section == 2: |
|
return QVariant('Compile flags') |
|
if section == 1: |
|
return QVariant('Found') |
|
return QVariant('Name') |
|
|
|
def data(self, index, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
row = index.row() |
|
column = index.column() |
|
return self.deps[row][column] |
|
|
|
def index(self, row, column, parent): |
|
return self.createIndex(row, column) |
|
|
|
def parent(self, index): |
|
return QModelIndex() |
|
|
|
class CoreModel(QAbstractItemModel): |
|
def __init__(self, core_data): |
|
super().__init__() |
|
self.elems = [] |
|
for langname, comp in core_data.compilers.items(): |
|
self.elems.append((langname + ' compiler', str(comp.get_exelist()))) |
|
for langname, comp in core_data.cross_compilers.items(): |
|
self.elems.append((langname + ' cross compiler', str(comp.get_exelist()))) |
|
|
|
def flags(self, index): |
|
return PyQt5.QtCore.Qt.ItemIsSelectable | PyQt5.QtCore.Qt.ItemIsEnabled |
|
|
|
def rowCount(self, index): |
|
if index.isValid(): |
|
return 0 |
|
return len(self.elems) |
|
|
|
def columnCount(self, index): |
|
return 2 |
|
|
|
def headerData(self, section, orientation, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
if section == 1: |
|
return QVariant('Value') |
|
return QVariant('Name') |
|
|
|
def data(self, index, role): |
|
if role != PyQt5.QtCore.Qt.DisplayRole: |
|
return QVariant() |
|
row = index.row() |
|
column = index.column() |
|
return self.elems[row][column] |
|
|
|
def index(self, row, column, parent): |
|
return self.createIndex(row, column) |
|
|
|
def parent(self, index): |
|
return QModelIndex() |
|
|
|
class OptionForm: |
|
def __init__(self, coredata, form): |
|
self.coredata = coredata |
|
self.form = form |
|
form.addRow(PyQt5.QtWidgets.QLabel("Meson options")) |
|
combo = QComboBox() |
|
combo.addItem('plain') |
|
combo.addItem('debug') |
|
combo.addItem('debugoptimized') |
|
combo.addItem('release') |
|
combo.setCurrentText(self.coredata.buildtype) |
|
combo.currentTextChanged.connect(self.build_type_changed) |
|
self.form.addRow('Build type', combo) |
|
strip = QCheckBox("") |
|
strip.setChecked(self.coredata.strip) |
|
strip.stateChanged.connect(self.strip_changed) |
|
self.form.addRow('Strip on install', strip) |
|
coverage = QCheckBox("") |
|
coverage.setChecked(self.coredata.coverage) |
|
coverage.stateChanged.connect(self.coverage_changed) |
|
self.form.addRow('Enable coverage', coverage) |
|
pch = QCheckBox("") |
|
pch.setChecked(self.coredata.use_pch) |
|
pch.stateChanged.connect(self.pch_changed) |
|
self.form.addRow('Enable pch', pch) |
|
unity = QCheckBox("") |
|
unity.setChecked(self.coredata.unity) |
|
unity.stateChanged.connect(self.unity_changed) |
|
self.form.addRow('Unity build', unity) |
|
form.addRow(PyQt5.QtWidgets.QLabel("Project options")) |
|
self.set_user_options() |
|
|
|
def set_user_options(self): |
|
options = self.coredata.user_options |
|
keys = list(options.keys()) |
|
keys.sort() |
|
self.opt_keys = keys |
|
self.opt_widgets = [] |
|
for key in keys: |
|
opt = options[key] |
|
if isinstance(opt, optinterpreter.UserStringOption): |
|
w = PyQt5.QtWidgets.QLineEdit(opt.value) |
|
w.textChanged.connect(self.user_option_changed) |
|
elif isinstance(opt, optinterpreter.UserBooleanOption): |
|
w = QCheckBox('') |
|
w.setChecked(opt.value) |
|
w.stateChanged.connect(self.user_option_changed) |
|
elif isinstance(opt, optinterpreter.UserComboOption): |
|
w = QComboBox() |
|
for i in opt.choices: |
|
w.addItem(i) |
|
w.setCurrentText(opt.value) |
|
w.currentTextChanged.connect(self.user_option_changed) |
|
else: |
|
raise RuntimeError("Unknown option type") |
|
self.opt_widgets.append(w) |
|
self.form.addRow(opt.description, w) |
|
|
|
def user_option_changed(self, dummy=None): |
|
for i in range(len(self.opt_keys)): |
|
key = self.opt_keys[i] |
|
w = self.opt_widgets[i] |
|
if isinstance(w, PyQt5.QtWidgets.QLineEdit): |
|
newval = w.text() |
|
elif isinstance(w, QComboBox): |
|
newval = w.currentText() |
|
elif isinstance(w, QCheckBox): |
|
if w.checkState() == 0: |
|
newval = False |
|
else: |
|
newval = True |
|
else: |
|
raise RuntimeError('Unknown widget type') |
|
self.coredata.user_options[key].set_value(newval) |
|
|
|
def build_type_changed(self, newtype): |
|
self.coredata.buildtype = newtype |
|
|
|
def strip_changed(self, newState): |
|
if newState == 0: |
|
ns = False |
|
else: |
|
ns = True |
|
self.coredata.strip = ns |
|
|
|
def coverage_changed(self, newState): |
|
if newState == 0: |
|
ns = False |
|
else: |
|
ns = True |
|
self.coredata.coverage = ns |
|
|
|
def pch_changed(self, newState): |
|
if newState == 0: |
|
ns = False |
|
else: |
|
ns = True |
|
self.coredata.use_pch = ns |
|
|
|
def unity_changed(self, newState): |
|
if newState == 0: |
|
ns = False |
|
else: |
|
ns = True |
|
self.coredata.unity = ns |
|
|
|
class ProcessRunner(): |
|
def __init__(self, rundir, cmdlist): |
|
self.cmdlist = cmdlist |
|
self.ui = uic.loadUi(os.path.join(priv_dir, 'mesonrunner.ui')) |
|
self.timer = QTimer(self.ui) |
|
self.timer.setInterval(1000) |
|
self.timer.timeout.connect(self.timeout) |
|
self.process = PyQt5.QtCore.QProcess() |
|
self.process.setProcessChannelMode(PyQt5.QtCore.QProcess.MergedChannels) |
|
self.process.setWorkingDirectory(rundir) |
|
self.process.readyRead.connect(self.read_data) |
|
self.process.finished.connect(self.finished) |
|
self.ui.termbutton.clicked.connect(self.terminated) |
|
self.return_value = 100 |
|
|
|
def run(self): |
|
self.process.start(self.cmdlist[0], self.cmdlist[1:]) |
|
self.timer.start() |
|
self.start_time = time.time() |
|
return self.ui.exec() |
|
|
|
def read_data(self): |
|
while(self.process.canReadLine()): |
|
txt = bytes(self.process.readLine()).decode('utf8') |
|
self.ui.console.append(txt) |
|
|
|
def finished(self): |
|
self.read_data() |
|
self.ui.termbutton.setText('Done') |
|
self.timer.stop() |
|
self.return_value = self.process.exitCode() |
|
|
|
def terminated(self, foo): |
|
self.process.kill() |
|
self.timer.stop() |
|
self.ui.done(self.return_value) |
|
|
|
def timeout(self): |
|
now = time.time() |
|
duration = int(now - self.start_time) |
|
msg = 'Elapsed time: %d:%d' % (duration // 60, duration % 60) |
|
self.ui.timelabel.setText(msg) |
|
|
|
class MesonGui(): |
|
def __init__(self, respawner, build_dir): |
|
self.respawner = respawner |
|
uifile = os.path.join(priv_dir, 'mesonmain.ui') |
|
self.ui = uic.loadUi(uifile) |
|
self.coredata_file = os.path.join(build_dir, 'meson-private/coredata.dat') |
|
self.build_file = os.path.join(build_dir, 'meson-private/build.dat') |
|
if not os.path.exists(self.coredata_file): |
|
print("Argument is not build directory.") |
|
sys.exit(1) |
|
self.coredata = pickle.load(open(self.coredata_file, 'rb')) |
|
self.build = pickle.load(open(self.build_file, 'rb')) |
|
self.build_dir = self.build.environment.build_dir |
|
self.src_dir = self.build.environment.source_dir |
|
self.build_models() |
|
self.options = OptionForm(self.coredata, self.ui.option_form) |
|
self.ui.show() |
|
|
|
def hide(self): |
|
self.ui.hide() |
|
|
|
def geometry(self): |
|
return self.ui.geometry() |
|
|
|
def move(self, x, y): |
|
return self.ui.move(x, y) |
|
|
|
def size(self): |
|
return self.ui.size() |
|
|
|
def resize(self, s): |
|
return self.ui.resize(s) |
|
|
|
def build_models(self): |
|
self.path_model = PathModel(self.coredata) |
|
self.target_model = TargetModel(self.build) |
|
self.dep_model = DependencyModel(self.coredata) |
|
self.core_model = CoreModel(self.coredata) |
|
self.fill_data() |
|
self.ui.core_view.setModel(self.core_model) |
|
hv = QHeaderView(1) |
|
hv.setModel(self.core_model) |
|
self.ui.core_view.setHeader(hv) |
|
self.ui.path_view.setModel(self.path_model) |
|
hv = QHeaderView(1) |
|
hv.setModel(self.path_model) |
|
self.ui.path_view.setHeader(hv) |
|
self.ui.target_view.setModel(self.target_model) |
|
hv = QHeaderView(1) |
|
hv.setModel(self.target_model) |
|
self.ui.target_view.setHeader(hv) |
|
self.ui.dep_view.setModel(self.dep_model) |
|
hv = QHeaderView(1) |
|
hv.setModel(self.dep_model) |
|
self.ui.dep_view.setHeader(hv) |
|
self.ui.compile_button.clicked.connect(self.compile) |
|
self.ui.test_button.clicked.connect(self.run_tests) |
|
self.ui.install_button.clicked.connect(self.install) |
|
self.ui.clean_button.clicked.connect(self.clean) |
|
self.ui.save_button.clicked.connect(self.save) |
|
|
|
def fill_data(self): |
|
self.ui.project_label.setText(self.build.projects['']) |
|
self.ui.srcdir_label.setText(self.src_dir) |
|
self.ui.builddir_label.setText(self.build_dir) |
|
if self.coredata.cross_file is None: |
|
btype = 'Native build' |
|
else: |
|
btype = 'Cross build' |
|
self.ui.buildtype_label.setText(btype) |
|
|
|
def run_process(self, cmdlist): |
|
cmdlist = [shutil.which(environment.detect_ninja())] + cmdlist |
|
dialog = ProcessRunner(self.build.environment.build_dir, cmdlist) |
|
dialog.run() |
|
# All processes (at the moment) may change cache state |
|
# so reload. |
|
self.respawner.respawn() |
|
|
|
def compile(self, foo): |
|
self.run_process([]) |
|
|
|
def run_tests(self, foo): |
|
self.run_process(['test']) |
|
|
|
def install(self, foo): |
|
self.run_process(['install']) |
|
|
|
def clean(self, foo): |
|
self.run_process(['clean']) |
|
|
|
def save(self, foo): |
|
pickle.dump(self.coredata, open(self.coredata_file, 'wb')) |
|
|
|
class Starter(): |
|
def __init__(self, sdir): |
|
uifile = os.path.join(priv_dir, 'mesonstart.ui') |
|
self.ui = uic.loadUi(uifile) |
|
self.ui.source_entry.setText(sdir) |
|
self.dialog = PyQt5.QtWidgets.QFileDialog() |
|
if len(sdir) == 0: |
|
self.dialog.setDirectory(os.getcwd()) |
|
else: |
|
self.dialog.setDirectory(sdir) |
|
self.ui.source_browse_button.clicked.connect(self.src_browse_clicked) |
|
self.ui.build_browse_button.clicked.connect(self.build_browse_clicked) |
|
self.ui.cross_browse_button.clicked.connect(self.cross_browse_clicked) |
|
self.ui.source_entry.textChanged.connect(self.update_button) |
|
self.ui.build_entry.textChanged.connect(self.update_button) |
|
self.ui.generate_button.clicked.connect(self.generate) |
|
self.update_button() |
|
self.ui.show() |
|
|
|
def generate(self): |
|
srcdir = self.ui.source_entry.text() |
|
builddir = self.ui.build_entry.text() |
|
cross = self.ui.cross_entry.text() |
|
cmdlist = [os.path.join(os.path.split(__file__)[0], 'meson.py'), srcdir, builddir] |
|
if cross != '': |
|
cmdlist += ['--cross', cross] |
|
pr = ProcessRunner(os.getcwd(), cmdlist) |
|
rvalue = pr.run() |
|
if rvalue == 0: |
|
os.execl(__file__, 'dummy', builddir) |
|
|
|
def update_button(self): |
|
if self.ui.source_entry.text() == '' or self.ui.build_entry.text() == '': |
|
self.ui.generate_button.setEnabled(False) |
|
else: |
|
self.ui.generate_button.setEnabled(True) |
|
|
|
def src_browse_clicked(self): |
|
self.dialog.setFileMode(2) |
|
if self.dialog.exec(): |
|
self.ui.source_entry.setText(self.dialog.selectedFiles()[0]) |
|
|
|
def build_browse_clicked(self): |
|
self.dialog.setFileMode(2) |
|
if self.dialog.exec(): |
|
self.ui.build_entry.setText(self.dialog.selectedFiles()[0]) |
|
|
|
def cross_browse_clicked(self): |
|
self.dialog.setFileMode(1) |
|
if self.dialog.exec(): |
|
self.ui.cross_entry.setText(self.dialog.selectedFiles()[0]) |
|
|
|
# Rather than rewrite all classes and arrays to be |
|
# updateable, just rebuild the entire GUI from |
|
# scratch whenever data on disk changes. |
|
|
|
class MesonGuiRespawner(): |
|
def __init__(self, arg): |
|
self.arg = arg |
|
self.gui = MesonGui(self, self.arg) |
|
|
|
def respawn(self): |
|
geo = self.gui.geometry() |
|
s = self.gui.size() |
|
self.gui.hide() |
|
self.gui = MesonGui(self, self.arg) |
|
self.gui.move(geo.x(), geo.y()) |
|
self.gui.resize(s) |
|
# Garbage collection takes care of the old gui widget |
|
|
|
if __name__ == '__main__': |
|
app = QApplication(sys.argv) |
|
if len(sys.argv) == 1: |
|
arg = "" |
|
elif len(sys.argv) == 2: |
|
arg = sys.argv[1] |
|
else: |
|
print(sys.argv[0], "<build or source dir>") |
|
sys.exit(1) |
|
if os.path.exists(os.path.join(arg, 'meson-private/coredata.dat')): |
|
guirespawner = MesonGuiRespawner(arg) |
|
else: |
|
runner = Starter(arg) |
|
sys.exit(app.exec_())
|
|
|