Create an interactive porting tool

`blueprint-compiler port` interactively ports a project to blueprint.
It will create the subproject wrap file, add it to .gitignore, decompile
your GtkBuilder XML files, emit code to copy and paste into your
meson.build file, and update POTFILES.in.

It can't quite handle all of the features the forward compiler can, so
it will skip those files.
This commit is contained in:
James Westman 2021-11-24 15:57:15 -06:00
parent 84dfe74755
commit 8e4433a487
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
19 changed files with 921 additions and 61 deletions

View file

@ -0,0 +1,387 @@
# decompiler.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import re
from enum import Enum
import typing as T
from dataclasses import dataclass
from .extensions import gtk_a11y
from .xml_reader import Element, parse
from .gir import *
from .utils import Colors
__all__ = ["decompile"]
_DECOMPILERS: T.Dict = {}
_CLOSING = {
"{": "}",
"[": "]",
}
_NAMESPACES = [
("GLib", "2.0"),
("GObject", "2.0"),
("Gio", "2.0"),
("Adw", "1.0"),
]
class LineType(Enum):
NONE = 1
STMT = 2
BLOCK_START = 3
BLOCK_END = 4
class DecompileCtx:
def __init__(self):
self._result = ""
self.gir = GirContext()
self._indent = 0
self._blocks_need_end = []
self._last_line_type = LineType.NONE
self.gir.add_namespace(get_namespace("Gtk", "4.0"))
@property
def result(self):
imports = "\n".join([
f"using {ns} {namespace.version};"
for ns, namespace in self.gir.namespaces.items()
])
return imports + "\n" + self._result
def type_by_cname(self, cname):
if type := self.gir.get_type_by_cname(cname):
return type
for ns, version in _NAMESPACES:
try:
namespace = get_namespace(ns, version)
if type := namespace.get_type_by_cname(cname):
self.gir.add_namespace(namespace)
return type
except:
pass
def start_block(self):
self._blocks_need_end.append(None)
def end_block(self):
if close := self._blocks_need_end.pop():
self.print(close)
def end_block_with(self, text):
self._blocks_need_end[-1] = text
def print(self, line, newline=True):
if line == "}" or line == "]":
self._indent -= 1
# Add blank lines between different types of lines, for neatness
if newline:
if line == "}" or line == "]":
line_type = LineType.BLOCK_END
elif line.endswith("{") or line.endswith("]"):
line_type = LineType.BLOCK_START
elif line.endswith(";"):
line_type = LineType.STMT
else:
line_type = LineType.NONE
if line_type != self._last_line_type and self._last_line_type != LineType.BLOCK_START and line_type != LineType.BLOCK_END:
self._result += "\n"
self._last_line_type = line_type
self._result += (" " * self._indent) + line
if newline:
self._result += "\n"
if line.endswith("{") or line.endswith("["):
if len(self._blocks_need_end):
self._blocks_need_end[-1] = _CLOSING[line[-1]]
self._indent += 1
def print_attribute(self, name, value, type):
if type is None:
self.print(f"{name}: \"{_escape_quote(value)}\";")
elif type.assignable_to(FloatType()):
self.print(f"{name}: {value};")
elif type.assignable_to(BoolType()):
val = _truthy(value)
self.print(f"{name}: {'true' if val else 'false'};")
elif (
type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Pixbuf"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Texture"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gdk.Paintable"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutAction"))
or type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("Gtk.ShortcutTrigger"))
):
self.print(f"{name}: \"{_escape_quote(value)}\";")
elif type.assignable_to(self.gir.namespaces["Gtk"].lookup_type("GObject.Object")):
self.print(f"{name}: {value};")
elif isinstance(type, Enumeration):
for member in type.members.values():
if member.nick == value or member.c_ident == value:
self.print(f"{name}: {member.name};")
break
else:
self.print(f"{name}: {value.replace('-', '_')};")
elif isinstance(type, Bitfield):
flags = re.sub(r"\s*\|\s*", " | ", value).replace("-", "_")
self.print(f"{name}: {flags};")
else:
self.print(f"{name}: \"{_escape_quote(value)}\";")
def _decompile_element(ctx: DecompileCtx, gir, xml):
try:
decompiler = _DECOMPILERS.get(xml.tag)
if decompiler is None:
raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")
args = {_canon(name): value for name, value in xml.attrs.items()}
if decompiler._cdata:
if len(xml.children):
args["cdata"] = None
else:
args["cdata"] = xml.cdata
ctx.start_block()
gir = decompiler(ctx, gir, **args)
for child_type in xml.children.values():
for child in child_type:
_decompile_element(ctx, gir, child)
ctx.end_block()
except UnsupportedError as e:
raise e
except TypeError as e:
raise UnsupportedError(tag=xml.tag)
def decompile(data):
ctx = DecompileCtx()
xml = parse(data)
_decompile_element(ctx, None, xml)
return ctx.result
def _canon(string: str) -> str:
if string == "class":
return "klass"
else:
return string.replace("-", "_").lower()
def _truthy(string: str) -> bool:
return string.lower() in ["yes", "true", "t", "y", "1"]
def _full_name(gir):
return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name
def _lookup_by_cname(gir, cname: str):
if isinstance(gir, GirContext):
return gir.get_type_by_cname(cname)
else:
return gir.get_containing(Repository).get_type_by_cname(cname)
def decompiler(tag, cdata=False):
def decorator(func):
func._cdata = cdata
_DECOMPILERS[tag] = func
return func
return decorator
def _escape_quote(string: str) -> str:
return (string
.replace("\\", "\\\\")
.replace("\'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n"))
@decompiler("interface")
def decompile_interface(ctx, gir):
return gir
@decompiler("requires")
def decompile_requires(ctx, gir, lib=None, version=None):
return gir
@decompiler("template")
def decompile_template(ctx, gir, klass, parent="Widget"):
gir_class = ctx.type_by_cname(parent)
if gir_class is None:
ctx.print(f"template {klass} : .{parent} {{")
else:
ctx.print(f"template {klass} : {_full_name(gir_class)} {{")
return gir_class
@decompiler("object")
def decompile_object(ctx, gir, klass, id=None):
gir_class = ctx.type_by_cname(klass)
klass_name = _full_name(gir_class) if gir_class is not None else "." + klass
if id is None:
ctx.print(f"{klass_name} {{")
else:
ctx.print(f"{klass_name} {id} {{")
return gir_class
@decompiler("child")
def decompile_child(ctx, gir, type=None):
if type is not None:
ctx.print(f"[{type}]")
return gir
@decompiler("property", cdata=True)
def decompile_property(ctx, gir, name, cdata, bind_source=None, bind_property=None, bind_flags=None, translatable="false", comments=None, context=None):
name = name.replace("_", "-")
if comments is not None:
ctx.print(f"/* Translators: {comments} */")
if cdata is None:
ctx.print(f"{name}: ", False)
ctx.end_block_with(";")
elif bind_source:
flags = ""
if bind_flags:
if "sync-create" in bind_flags:
flags += " sync-create"
if "after" in bind_flags:
flags += " after"
ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
elif _truthy(translatable):
if context is not None:
ctx.print(f"{name}: C_(\"{_escape_quote(context)}\", \"{_escape_quote(cdata)}\");")
else:
ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");")
elif gir is None or gir.properties.get(name) is None:
ctx.print(f"{name}: \"{_escape_quote(cdata)}\";")
else:
ctx.print_attribute(name, cdata, gir.properties.get(name).type)
return gir
@decompiler("signal")
def decompile_signal(ctx, gir, name, handler, swapped="false"):
name = name.replace("_", "-")
if _truthy(swapped):
ctx.print(f"{name} => {handler}() swapped;")
else:
ctx.print(f"{name} => {handler}();")
return gir
@decompiler("style")
def decompile_style(ctx, gir):
ctx.print(f"styles [")
@decompiler("class")
def decompile_class(ctx, gir, name):
ctx.print(f'"{name}",')
@decompiler("layout")
def decompile_layout(ctx, gir):
ctx.print("layout {")
@decompiler("menu")
def decompile_menu(ctx, gir, id=None):
if id:
ctx.print(f"menu {id} {{")
else:
ctx.print("menu {")
@decompiler("submenu")
def decompile_submenu(ctx, gir, id=None):
if id:
ctx.print(f"submenu {id} {{")
else:
ctx.print("submenu {")
@decompiler("item")
def decompile_item(ctx, gir, id=None):
if id:
ctx.print(f"item {id} {{")
else:
ctx.print("item {")
@decompiler("section")
def decompile_section(ctx, gir, id=None):
if id:
ctx.print(f"section {id} {{")
else:
ctx.print("section {")
@decompiler("attribute", cdata=True)
def decompile_attribute(ctx, gir, name, cdata, translatable="false", comments=None, context=None):
decompile_property(ctx, gir, name, cdata, translatable=translatable, comments=comments, context=context)
@decompiler("accessibility")
def decompile_accessibility(ctx, gir):
ctx.print("accessibility {")
@decompiler("attributes")
def decompile_attributes(ctx, gir):
ctx.print("attributes {")
@decompiler("relation", cdata=True)
def decompile_relation(ctx, gir, name, cdata):
ctx.print_attribute(name, cdata, gtk_a11y.get_types(ctx.gir).get(name))
@decompiler("state", cdata=True)
def decompile_state(ctx, gir, name, cdata, translatable="false"):
if _truthy(translatable):
ctx.print(f"{name}: _(\"{_escape_quote(cdata)}\");")
else:
ctx.print_attribute(name, cdata, gtk_a11y.get_types(ctx.gir).get(name))
@dataclass
class UnsupportedError(Exception):
message: str = "unsupported feature"
tag: T.Optional[str] = None
def print(self, filename: str):
print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}")
print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
if self.tag:
print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
print(f"""{Colors.FAINT}The gtk-blueprint-tool compiler might support this feature, but the
porting tool does not. You probably need to port this file manually.{Colors.CLEAR}\n""")

View file

@ -21,16 +21,7 @@ from dataclasses import dataclass
import typing as T
import sys, traceback
from . import utils
class _colors:
RED = '\033[91m'
YELLOW = '\033[33m'
FAINT = '\033[2m'
BOLD = '\033[1m'
BLUE = '\033[34m'
UNDERLINE = '\033[4m'
CLEAR = '\033[0m'
from .utils import Colors
class PrintableError(Exception):
""" Parent class for errors that can be pretty-printed for the user, e.g.
@ -85,12 +76,12 @@ class CompileError(PrintableError):
# Display 1-based line numbers
line_num += 1
print(f"""{_colors.RED}{_colors.BOLD}{self.category}: {self.message}{_colors.CLEAR}
print(f"""{Colors.RED}{Colors.BOLD}{self.category}: {self.message}{Colors.CLEAR}
at {filename} line {line_num} column {col_num}:
{_colors.FAINT}{line_num :>4} |{_colors.CLEAR}{line.rstrip()}\n {_colors.FAINT}|{" "*(col_num-1)}^{_colors.CLEAR}""")
{Colors.FAINT}{line_num :>4} |{Colors.CLEAR}{line.rstrip()}\n {Colors.FAINT}|{" "*(col_num-1)}^{Colors.CLEAR}""")
for hint in self.hints:
print(f"{_colors.FAINT}hint: {hint}{_colors.CLEAR}")
print(f"{Colors.FAINT}hint: {hint}{Colors.CLEAR}")
print()
@ -130,14 +121,14 @@ def assert_true(truth: bool, message:str=None):
raise CompilerBugError(message)
def report_compile_error():
def report_bug():
""" Report an error and ask people to report it. """
print(traceback.format_exc())
print(f"Arguments: {sys.argv}\n")
print(f"""{_colors.BOLD}{_colors.RED}***** COMPILER BUG *****
print(f"""{Colors.BOLD}{Colors.RED}***** COMPILER BUG *****
The blueprint-compiler program has crashed. Please report the above stacktrace,
along with the input file(s) if possible, on GitLab:
{_colors.BOLD}{_colors.BLUE}{_colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue
{_colors.CLEAR}""")
{Colors.BOLD}{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue
{Colors.CLEAR}""")

View file

@ -27,7 +27,7 @@ from ..parser_utils import *
from ..xml_emitter import XmlEmitter
def _get_property_types(gir):
def get_property_types(gir):
# from <https://docs.gtk.org/gtk4/enum.AccessibleProperty.html>
return {
"autocomplete": gir.get_type("AccessibleAutocomplete", "Gtk"),
@ -52,7 +52,7 @@ def _get_property_types(gir):
}
def _get_relation_types(gir):
def get_relation_types(gir):
# from <https://docs.gtk.org/gtk4/enum.AccessibleRelation.html>
widget = gir.get_type("Widget", "Gtk")
return {
@ -77,7 +77,7 @@ def _get_relation_types(gir):
}
def _get_state_types(gir):
def get_state_types(gir):
# from <https://docs.gtk.org/gtk4/enum.AccessibleState.html>
return {
"busy": BoolType(),
@ -90,11 +90,11 @@ def _get_state_types(gir):
"selected": BoolType(),
}
def _get_types(gir):
def get_types(gir):
return {
**_get_property_types(gir),
**_get_relation_types(gir),
**_get_state_types(gir),
**get_property_types(gir),
**get_relation_types(gir),
**get_state_types(gir),
}
def _get_docs(gir, name):
@ -123,22 +123,22 @@ class A11yProperty(BaseTypedAttribute):
def tag_name(self):
name = self.tokens["name"]
gir = self.root.gir
if name in _get_property_types(gir):
if name in get_property_types(gir):
return "property"
elif name in _get_relation_types(gir):
elif name in get_relation_types(gir):
return "relation"
elif name in _get_state_types(gir):
elif name in get_state_types(gir):
return "state"
else:
raise CompilerBugError()
@property
def value_type(self) -> GirType:
return _get_types(self.root.gir).get(self.tokens["name"])
return get_types(self.root.gir).get(self.tokens["name"])
@validate("name")
def is_valid_property(self):
types = _get_types(self.root.gir)
types = get_types(self.root.gir)
if self.tokens["name"] not in types:
raise CompileError(
f"'{self.tokens['name']}' is not an accessibility property, relation, or state",
@ -147,7 +147,7 @@ class A11yProperty(BaseTypedAttribute):
@docs("name")
def prop_docs(self):
if self.tokens["name"] in _get_types(self.root.gir):
if self.tokens["name"] in get_types(self.root.gir):
return _get_docs(self.root.gir, self.tokens["name"])
@ -186,5 +186,5 @@ def a11y_completer(ast_node, match_variables):
matches=new_statement_patterns,
)
def a11y_name_completer(ast_node, match_variables):
for name, type in _get_types(ast_node.root.gir).items():
for name, type in get_types(ast_node.root.gir).items():
yield Completion(name, CompletionItemKind.Property, docs=_get_docs(ast_node.root.gir, type))

View file

@ -142,6 +142,10 @@ class GirNode:
def name(self) -> str:
return self.xml["name"]
@lazy_prop
def cname(self) -> str:
return self.xml["c:type"]
@lazy_prop
def available_in(self) -> str:
return self.xml.get("version")
@ -176,10 +180,6 @@ class Property(GirNode):
def __init__(self, klass, xml: xml_reader.Element):
super().__init__(klass, xml)
@property
def type_name(self):
return self.xml.get_elements('type')[0]['name']
@property
def signature(self):
return f"{self.type_name} {self.container.name}.{self.name}"
@ -295,12 +295,16 @@ class EnumMember(GirNode):
def nick(self):
return self.xml["glib:nick"]
@property
def c_ident(self):
return self.xml["c:identifier"]
@property
def signature(self):
return f"enum member {self.full_name} = {self.value}"
class Enumeration(GirNode):
class Enumeration(GirNode, GirType):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") }
@ -309,6 +313,36 @@ class Enumeration(GirNode):
def signature(self):
return f"enum {self.full_name}"
def assignable_to(self, type):
return type == self
class BitfieldMember(GirNode):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self._value = xml["value"]
@property
def value(self):
return self._value
@property
def signature(self):
return f"bitfield member {self.full_name} = {bin(self.value)}"
class Bitfield(GirNode, GirType):
def __init__(self, ns, xml: xml_reader.Element):
super().__init__(ns, xml)
self.members = { child["name"]: EnumMember(self, child) for child in xml.get_elements("member") }
@property
def signature(self):
return f"bitfield {self.full_name}"
def assignable_to(self, type):
return type == self
class Namespace(GirNode):
def __init__(self, repo, xml: xml_reader.Element):
@ -316,6 +350,7 @@ class Namespace(GirNode):
self.classes = { child["name"]: Class(self, child) for child in xml.get_elements("class") }
self.interfaces = { child["name"]: Interface(self, child) for child in xml.get_elements("interface") }
self.enumerations = { child["name"]: Enumeration(self, child) for child in xml.get_elements("enumeration") }
self.bitfields = { child["name"]: Bitfield(self, child) for child in xml.get_elements("bitfield") }
self.version = xml["version"]
@property
@ -325,7 +360,19 @@ class Namespace(GirNode):
def get_type(self, name):
""" Gets a type (class, interface, enum, etc.) from this namespace. """
return self.classes.get(name) or self.interfaces.get(name) or self.enumerations.get(name)
return (
self.classes.get(name)
or self.interfaces.get(name)
or self.enumerations.get(name)
or self.bitfields.get(name)
)
def get_type_by_cname(self, cname: str):
""" Gets a type from this namespace by its C name. """
for item in [*self.classes.values(), *self.interfaces.values(), *self.enumerations.values()]:
if item.cname == cname:
return item
def lookup_type(self, type_name: str):
@ -359,6 +406,13 @@ class Repository(GirNode):
return self.lookup_namespace(ns).get_type(name)
def get_type_by_cname(self, name: str) -> T.Optional[GirNode]:
for ns in self.namespaces.values():
if type := ns.get_type_by_cname(name):
return type
return None
def lookup_namespace(self, ns: str):
""" Finds a namespace among this namespace's dependencies. """
if namespace := self.namespaces.get(ns):
@ -382,6 +436,13 @@ class GirContext:
self.namespaces[namespace.name] = namespace
def get_type_by_cname(self, name: str) -> T.Optional[GirNode]:
for ns in self.namespaces.values():
if type := ns.get_type_by_cname(name):
return type
return None
def get_type(self, name: str, ns: str) -> T.Optional[GirNode]:
ns = ns or "Gtk"

View file

@ -0,0 +1,285 @@
# interactive_port.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
import difflib
import os
from . import decompiler, tokenizer, parser
from .errors import MultipleErrors, PrintableError
from .utils import Colors
# A tool to interactively port projects to blueprints.
class CouldNotPort:
def __init__(self, message):
self.message = message
def change_suffix(f):
return f.removesuffix(".ui") + ".blp"
def decompile_file(in_file, out_file) -> T.Union[str, CouldNotPort]:
if os.path.exists(out_file):
return CouldNotPort("already exists")
try:
decompiled = decompiler.decompile(in_file)
try:
# make sure the output compiles
tokens = tokenizer.tokenize(decompiled)
ast, errors = parser.parse(tokens)
if errors:
raise errors
if len(ast.errors):
raise MultipleErrors(ast.errors)
ast.generate()
except PrintableError as e:
e.pretty_print(out_file, decompiled)
print(f"{Colors.RED}{Colors.BOLD}error: the generated file does not compile{Colors.CLEAR}")
print(f"in {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE}")
print(
f"""{Colors.FAINT}Either the original XML file had an error, or there is a bug in the
porting tool. If you think it's a bug (which is likely), please file an issue on GitLab:
{Colors.BLUE}{Colors.UNDERLINE}https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/new?issue{Colors.CLEAR}\n""")
return CouldNotPort("does not compile")
return decompiled
except decompiler.UnsupportedError as e:
e.print(in_file)
return CouldNotPort("could not convert")
def listdir_recursive(subdir):
files = os.listdir(subdir)
for file in files:
full = os.path.join(subdir, file)
if full == "./subprojects":
# skip the subprojects directory
return
if os.path.isfile(full):
yield full
elif os.path.isdir(full):
yield from listdir_recursive(full)
def yesno(prompt):
while True:
response = input(f"{Colors.BOLD}{prompt} [y/n] {Colors.CLEAR}")
if response.lower() in ["yes", "y"]:
return True
elif response.lower() in ["no", "n"]:
return False
def enter():
input(f"{Colors.BOLD}Press Enter when you have done that: {Colors.CLEAR}")
def step1():
print(f"{Colors.BOLD}STEP 1: Create subprojects/blueprint-compiler.wrap{Colors.CLEAR}")
if os.path.exists("subprojects/blueprint-compiler.wrap"):
print("subprojects/blueprint-compiler.wrap already exists, skipping\n")
return
if yesno("Create subprojects/blueprint-compiler.wrap?"):
try:
os.mkdir("subprojects")
except:
pass
with open("subprojects/blueprint-compiler.wrap", "w") as wrap:
wrap.write("""[wrap-git]
directory = blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
revision = main
depth = 1
[provide]
program_names = blueprint-compiler""")
print()
def step2():
print(f"{Colors.BOLD}STEP 2: Set up .gitignore{Colors.CLEAR}")
if os.path.exists(".gitignore"):
with open(".gitignore", "r+") as gitignore:
ignored = [line.strip() for line in gitignore.readlines()]
if "/subprojects/blueprint-compiler" not in ignored:
if yesno("Add '/subprojects/blueprint-compiler' to .gitignore?"):
gitignore.write("\n/subprojects/blueprint-compiler\n")
else:
print("'/subprojects/blueprint-compiler' already in .gitignore, skipping")
else:
if yesno("Create .gitignore with '/subprojects/blueprint-compiler'?"):
with open(".gitignore", "w") as gitignore:
gitignore.write("/subprojects/blueprint-compiler\n")
print()
def step3():
print(f"{Colors.BOLD}STEP 3: Convert UI files{Colors.CLEAR}")
files = [
(file, change_suffix(file), decompile_file(file, change_suffix(file)))
for file in listdir_recursive(".")
if file.endswith(".ui")
]
success = 0
for in_file, out_file, result in files:
if isinstance(result, CouldNotPort):
if result.message == "already exists":
print(Colors.FAINT, end="")
print(f"{Colors.RED}will not port {Colors.UNDERLINE}{in_file}{Colors.NO_UNDERLINE} -> {Colors.UNDERLINE}{out_file}{Colors.NO_UNDERLINE} [{result.message}]{Colors.CLEAR}")
else:
print(f"will port {Colors.UNDERLINE}{in_file}{Colors.CLEAR} -> {Colors.UNDERLINE}{out_file}{Colors.CLEAR}")
success += 1
print()
if len(files) == 0:
print(f"{Colors.RED}No UI files found.{Colors.CLEAR}")
elif success == len(files):
print(f"{Colors.GREEN}All files were converted.{Colors.CLEAR}")
elif success > 0:
print(f"{Colors.RED}{success} file(s) were converted, {len(files) - success} were not.{Colors.CLEAR}")
else:
print(f"{Colors.RED}None of the files could be converted.{Colors.CLEAR}")
if success > 0 and yesno("Save these changes?"):
for in_file, out_file, result in files:
if not isinstance(result, CouldNotPort):
with open(out_file, "x") as file:
file.write(result)
print()
results = [
(in_file, out_file)
for in_file, out_file, result in files
if not isinstance(result, CouldNotPort) or result.message == "already exists"
]
if len(results):
return zip(*results)
else:
return ([], [])
def step4(ported):
print(f"{Colors.BOLD}STEP 4: Set up meson.build{Colors.CLEAR}")
print(f"{Colors.BOLD}{Colors.YELLOW}NOTE: Depending on your build system setup, you may need to make some adjustments to this step.{Colors.CLEAR}")
meson_files = [file for file in listdir_recursive(".") if os.path.basename(file) == "meson.build"]
for meson_file in meson_files:
with open(meson_file, "r") as f:
if "gnome.compile_resources" in f.read():
parent = os.path.dirname(meson_file)
file_list = "\n ".join([
f"'{os.path.relpath(file, parent)}',"
for file in ported
if file.startswith(parent)
])
if len(file_list):
print(f"{Colors.BOLD}Paste the following into {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}")
print(f"""
blueprints = custom_target('blueprints',
input: files(
{file_list}
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
)
""")
enter()
print(f"""{Colors.BOLD}Paste the following into the 'gnome.compile_resources()'
arguments in {Colors.UNDERLINE}{meson_file}{Colors.NO_UNDERLINE}:{Colors.CLEAR}
dependencies: blueprints,
""")
enter()
print()
def step5(in_files):
print(f"{Colors.BOLD}STEP 5: Update POTFILES.in{Colors.CLEAR}")
if not os.path.exists("po/POTFILES.in"):
print(f"{Colors.UNDERLINE}po/POTFILES.in{Colors.NO_UNDERLINE} does not exist, skipping\n")
return
with open("po/POTFILES.in", "r") as potfiles:
old_lines = potfiles.readlines()
lines = old_lines.copy()
for in_file in in_files:
for i, line in enumerate(lines):
if line.strip() == in_file.removeprefix("./"):
lines[i] = change_suffix(line.strip()) + "\n"
new_data = "".join(lines)
print(f"{Colors.BOLD}Will make the following changes to {Colors.UNDERLINE}po/POTFILES.in{Colors.CLEAR}")
print(
"".join([
(Colors.GREEN if line.startswith('+') else Colors.RED + Colors.FAINT if line.startswith('-') else '') + line + Colors.CLEAR
for line in difflib.unified_diff(old_lines, lines)
])
)
if yesno("Is this ok?"):
with open("po/POTFILES.in", "w") as potfiles:
potfiles.writelines(lines)
print()
def step6(in_files):
print(f"{Colors.BOLD}STEP 6: Clean up{Colors.CLEAR}")
if yesno("Delete old XML files?"):
for file in in_files:
try:
os.remove(file)
except:
pass
def run(opts):
step1()
step2()
in_files, out_files = step3()
step4(out_files)
step5(in_files)
step6(in_files)
print(f"{Colors.BOLD}STEP 6: Done! Make sure your app still builds and runs correctly.{Colors.CLEAR}")

View file

@ -20,9 +20,10 @@
import argparse, json, os, sys
from .errors import PrintableError, report_compile_error, MultipleErrors
from .errors import PrintableError, report_bug, MultipleErrors
from .lsp import LanguageServer
from . import parser, tokenizer
from . import parser, tokenizer, decompiler, interactive_port
from .utils import Colors
from .xml_emitter import XmlEmitter
@ -44,6 +45,8 @@ class BlueprintApp:
batch_compile.add_argument("input_dir", metavar="input-dir")
batch_compile.add_argument("inputs", nargs="+", metavar="filenames", default=sys.stdin, type=argparse.FileType('r'))
port = self.add_subcommand("port", "Interactive porting tool", self.cmd_port)
lsp = self.add_subcommand("lsp", "Run the language server (for internal use by IDEs)", self.cmd_lsp)
lsp.add_argument("--logfile", dest="logfile", default=None, type=argparse.FileType('a'))
@ -54,8 +57,12 @@ class BlueprintApp:
opts.func(opts)
except SystemExit as e:
raise e
except KeyboardInterrupt:
print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}")
except EOFError:
print(f"\n\n{Colors.RED}{Colors.BOLD}Interrupted.{Colors.CLEAR}")
except:
report_compile_error()
report_bug()
def add_subcommand(self, name, help, func):
@ -114,6 +121,10 @@ class BlueprintApp:
langserv.run()
def cmd_port(self, opts):
interactive_port.run(opts)
def _compile(self, data: str) -> str:
tokens = tokenizer.tokenize(data)
ast, errors = parser.parse(tokens)

View file

@ -20,6 +20,18 @@
import typing as T
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[33m'
FAINT = '\033[2m'
BOLD = '\033[1m'
BLUE = '\033[34m'
UNDERLINE = '\033[4m'
NO_UNDERLINE = '\033[24m'
CLEAR = '\033[0m'
def lazy_prop(func):
key = "_lazy_prop_" + func.__name__

View file

@ -19,6 +19,7 @@
from collections import defaultdict
import typing as T
from xml import sax
from .utils import lazy_prop
@ -28,22 +29,22 @@ from .utils import lazy_prop
PARSE_GIR = set([
"repository", "namespace", "class", "interface", "property", "glib:signal",
"include", "implements", "type", "parameter", "parameters", "enumeration",
"member",
"member", "bitfield",
])
class Element:
def __init__(self, tag, attrs):
def __init__(self, tag, attrs: T.Dict[str, str]):
self.tag = tag
self.attrs = attrs
self.children = defaultdict(list)
self.cdata_chunks = []
self.children: T.Dict[str, T.List["Element"]] = defaultdict(list)
self.cdata_chunks: T.List[str] = []
@lazy_prop
def cdata(self):
return ''.join(self.cdata_chunks)
def get_elements(self, name):
def get_elements(self, name) -> T.List["Element"]:
return self.children.get(name, [])
def __getitem__(self, key):
@ -58,7 +59,7 @@ class Handler(sax.handler.ContentHandler):
self._interesting_elements = parse_type
def startElement(self, name, attrs):
if name not in self._interesting_elements:
if self._interesting_elements is not None and name not in self._interesting_elements:
self.skipping += 1
if self.skipping > 0:
return
@ -77,7 +78,7 @@ class Handler(sax.handler.ContentHandler):
def endElement(self, name):
if self.skipping == 0:
self.stack.pop()
if name not in self._interesting_elements:
if self._interesting_elements is not None and name not in self._interesting_elements:
self.skipping -= 1
def characters(self, content):
@ -85,7 +86,7 @@ class Handler(sax.handler.ContentHandler):
self.stack[-1].cdata_chunks.append(content)
def parse(filename, parse_type):
def parse(filename, parse_type=None):
parser = sax.make_parser()
handler = Handler(parse_type)
parser.setContentHandler(handler)