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
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}")
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue