mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
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:
parent
84dfe74755
commit
8e4433a487
19 changed files with 921 additions and 61 deletions
387
blueprintcompiler/decompiler.py
Normal file
387
blueprintcompiler/decompiler.py
Normal 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""")
|
|
@ -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}""")
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
285
blueprintcompiler/interactive_port.py
Normal file
285
blueprintcompiler/interactive_port.py
Normal 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}")
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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__
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue