meson-to-hermetic: Rename meson-to-hermetic to -> meson_to_hermetic

Python packages does not allow for the use of hyphen as it's a reserved token.
This change needs to be made to be imported by unit-tests in the future

This change will require a new venv to be generated locally:
`$ rm -rf venv`
`$ ./setup-venv.sh`
`$ source venv/bin/activate`

Test: Run meson_to_hermetic/build-android/fuchsia-turnip.sh
Bug: 360173803
Change-Id: Ie6fab4689ef5fc985e15747b0c06bfae662c387c
This commit is contained in:
= 2024-08-26 21:22:19 +00:00
parent 8e6d10e737
commit c6cc3b175f
15 changed files with 7 additions and 7 deletions

1
meson_to_hermetic/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
venv/

View file

@ -0,0 +1,42 @@
# meson-to-hermetic: automated build system generation
Goal: ease the integration of Mesa as a component inside larger projects like
Android (AOSP).
How: parse the meson build tree into a python build script, which can be used
together with configuration and options to generate other kinds of build scripts.
Status: useful, but rough. Supports Android (Soong) and Fuchsia (Bazel).
## Python Dependencies
- Python 3.11+
- See `requirements.txt`
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
## Environment Setup
1. Open a terminal within `mesa3d/meson_to_hermetic`
2. Run the `setup-venv.sh` file to automatically create a python3 venv and install dependencies.
## Linting the code
1. Before pushing code for review; run `lint.sh` to automatically lint all of the python scripts.
- IMPORTANT: Run the lint.sh from the `meson_to_hermetic` directory.
## 1 - Generate python from meson
generate_python_build.py: reads meson.build files (following subdir() commands)
and uses meson2python to transform the meson into python.
meson2python.py: passes meson.build input and the meson grammar to
[python lark](https://github.com/lark-parser/lark) to perform lexing and parsing; then
transforms the parse tree into valid python. The result is one large python script.
## 2 - Generate Android.bp from python
meson_android.py defines the meson API entry points and emits Android.bp build
constructs. A config file is read to determine some build parameters such as
`cpu_family` (similar to meson's cross file).
## Limitations
Meson build options must be set by modifying the defaults in meson_options.txt.

View file

@ -0,0 +1,67 @@
# Copyright 2024 Google LLC
# SPDX-License-Identifier: Apache-2.0
build = 'Soong'
[[project_config]]
name = 'android_aarch64_drivers'
[project_config.host_machine]
cpu_family = 'aarch64'
cpu = 'aarch64'
host_machine = 'android'
build_machine = 'linux'
[project_config.meson_options]
platforms = 'android'
android-libbacktrace = 'disabled'
gallium-drivers = ''
vulkan-drivers = 'freedreno'
freedreno-kmds = 'kgsl'
platform-sdk-version = 33
[project_config.header_not_supported]
headers = []
[project_config.symbol_not_supported]
symbols = []
[project_config.function_not_supported]
functions = []
[project_config.link_not_supported]
links = []
[project_config.ext_dependencies]
# DependencyTargetType
# SHARED_LIBRARY = 1
# STATIC_LIBRARY = 2
# HEADER_LIBRARY = 3
# See meson_impl.py
zlib = [
{ target_name = 'libz', target_type = 2 }
]
hardware = [
{ target_name = 'libhardware', target_type = 1 },
{ target_name = 'hwvulkan_headers', target_type = 3 }
]
cutils = [
{ target_name = 'libcutils', target_type = 1 }
]
log = [
{ target_name = 'liblog', target_type = 1 }
]
nativewindow = [
{ target_name = 'libnativewindow', target_type = 1 }
]
sync = [
{ target_name = 'libsync', target_type = 2 }
]
'android.hardware.graphics.mapper' = [
{ target_name = 'libgralloctypes', target_type = 2 },
{ target_name = 'android.hardware.graphics.mapper@4.0', target_type = 2 },
{ target_name = 'libhidlbase', target_type = 1 },
{ target_name = 'libutils', target_type = 1 }
]
# Define new project configs
# [[project_config]]

View file

@ -0,0 +1,35 @@
#!/bin/bash
set -e
if [ ! -f meson_options.txt ]; then
echo "Run this script from the repo root"
exit 1
fi
BIN_DIR=$(dirname "$0")
ROOT_DIR=$BIN_DIR/../../..
PYTHON_BUILD=generate_android_build.py
REGEN=0
if [ "$1" == "-regen" ]; then
REGEN=1
fi
if [ ! -f $PYTHON_BUILD ]; then
REGEN=1
fi
if [ "$REGEN" == "1" ]; then
time python3 $BIN_DIR/generate_python_build.py
else
echo "Python build found; use -regen to regenerate it"
fi
# Always generate Android.bp because it's fast
PYTHONPATH=$BIN_DIR python3 generate_android_build.py --config=meson_to_hermetic/aosp.toml
source $ROOT_DIR/build/envsetup.sh
lunch aosp_trout_arm64-trunk_staging-userdebug
m vulkan_freedreno

View file

@ -0,0 +1,57 @@
#!/bin/bash
set -e
if [ ! -f meson_options.txt ]; then
echo "Run this script from the repo root"
exit 1
fi
BIN_DIR=$(dirname "$0")
ROOT_DIR=$BIN_DIR/../../..
PYTHON_BUILD=generate_fuchsia_build.py
function unwrap_zlib {
rm -rf fuchsia-build/third_party/download fuchsia-build/third_party/zlib-*
SOURCE_URL=`grep -Eo 'http://[^ ]+zlib[0-9\.-]+\.tar\.gz' subprojects/zlib.wrap`
wget -P fuchsia-build/third_party/download $SOURCE_URL
tar -C fuchsia-build/third_party -xf fuchsia-build/third_party/download/zlib-*
PATCH_URL=`grep -Eo 'https://[^ ]+get_patch' subprojects/zlib.wrap`
wget -P fuchsia-build/third_party/download $PATCH_URL
# get_patch is a zip file
unzip -d fuchsia-build/third_party fuchsia-build/third_party/download/get_patch
pushd fuchsia-build/third_party/zlib-*
# Create an empty workspace
touch WORKSPACE.bazel
ln -s ../../../meson_to_hermetic meson_to_hermetic
python3 meson_to_hermetic/generate_python_build.py --target fuchsia
PYTHONPATH=$PWD/meson_to_hermetic python3 generate_fuchsia_build.py --config=meson_to_hermetic/fuchsia.toml
popd
}
REGEN=0
if [ "$1" == "-regen" ]; then
REGEN=1
fi
if [ ! -f $PYTHON_BUILD ]; then
REGEN=1
fi
if [ ! -d fuchsia-build/third_party/zlib-* ]; then
REGEN=1
fi
if [ "$REGEN" == "1" ]; then
unwrap_zlib
time python3 $BIN_DIR/generate_python_build.py --target fuchsia
else
echo "Python build found; use -regen to regenerate it"
fi
# Always generate Android.bp because it's fast
PYTHONPATH=$BIN_DIR python3 generate_fuchsia_build.py --config=meson_to_hermetic/fuchsia.toml
$BIN_DIR/../tools/bazel --bazelrc=fuchsia-build/third_party/fuchsia-infra-bazel-rules/config/common_config.bazelrc \
build --config=fuchsia_arm64 --platforms=@fuchsia_sdk//fuchsia/constraints/platforms:fuchsia_arm64 \
//:vulkan_freedreno

View file

@ -0,0 +1,60 @@
# Copyright 2024 Google LLC
# SPDX-License-Identifier: Apache-2.0
build = 'bazel'
[[project_config]]
name = 'fuchsia_aarch64_drivers'
[project_config.host_machine]
cpu_family = 'aarch64'
cpu = 'aarch64'
host_machine = 'fuchsia'
build_machine = 'linux'
[project_config.meson_options]
platforms = 'none'
gallium-drivers = ''
vulkan-drivers = 'freedreno'
freedreno-kmds = 'magma'
platform-sdk-version = 33
shader-cache = 'disabled'
[project_config.header_not_supported]
headers = [
'sys/sysmacros.h',
]
[project_config.symbol_not_supported]
symbols = [
]
[project_config.function_not_supported]
functions = [
'getrandom',
'memfd_create',
]
[project_config.link_not_supported]
links = [
'strtod has locale support',
]
[project_config.ext_dependencies]
# DependencyTargetType
# SHARED_LIBRARY = 1
# STATIC_LIBRARY = 2
# HEADER_LIBRARY = 3
# See meson_impl.py
zlib = [
{ target_name = '@zlib//:zlib', target_type = 2 }
]
libmagma = [
{ target_name = '@fuchsia_sdk//pkg/magma_client', target_type = 2 }
]
libmagma_virt = [
# No targets
]
# Define new project configs
# [[project_config]]

View file

@ -0,0 +1,141 @@
import getopt
import os
import re
import sys
from meson2python import meson2python
from jinja2 import Environment, FileSystemLoader
from pathlib import Path
environment = Environment(
loader=FileSystemLoader(Path(__file__).parent.resolve() / 'templates/')
)
generator_template = environment.get_template('generate_python_build.txt')
# Converts the given |file_name| from meson to python, and writes the python code
# to the given |file|. Code is indented by |output_indent|. When a subdir command
# is found, the meson.build build in that subdir is converted by recursively invoking
# this function.
def process_meson(file_name: str, output_indent: str = ''):
python_code = ''
python_code += (
output_indent
+ '########################################################################################################################'
)
python_code += '\n' + output_indent + f'### Begin conversion from: {file_name}'
python_code += (
'\n'
+ output_indent
+ '########################################################################################################################'
)
print('Processing: ' + file_name)
sys.stdout.flush()
content = meson2python(file_name)
inside_literal = False
for line in content.splitlines():
# Remove line terminator
line = line.rstrip()
# Check for multiline literals.
# We ignore literals that start and end on one line, though that may cause
# problems for the line processing below.
matches = re.findall(r"'''", line)
literal_delimiter_count = len(matches)
line_prefix = ''
line_suffix = ''
if literal_delimiter_count == 1:
inside_literal = not inside_literal
literal_line_split = line.split(r"'''")
if inside_literal:
line = literal_line_split[0]
line_suffix = r"'''" + literal_line_split[1]
else:
line_prefix = literal_line_split[0] + r"'''"
line = literal_line_split[1]
elif literal_delimiter_count == 0 or literal_delimiter_count == 2:
if inside_literal:
# Don't match anything while inside literal
line_prefix = line
line = ''
else:
exit('Unhandled literal in line: ' + line)
# Recurse into subdirs
match = re.match("( *)subdir\('([a-zA-Z0-9_/]+)'\)", line)
if match is not None:
subdir_output_indent = match.group(1) + output_indent
current_dir = os.path.dirname(file_name)
next_dir = os.path.join(current_dir, match.group(2))
next_file = os.path.join(next_dir, 'meson.build')
# Ensure the build definitions are aware of the changing directory
python_code += f"\n{subdir_output_indent}set_relative_dir('{next_dir}')"
python_code += '\n' + process_meson(next_file, subdir_output_indent)
python_code += f"\n{subdir_output_indent}set_relative_dir('{current_dir}')"
continue
python_code += f'\n{output_indent + line_prefix + line + line_suffix}'
python_code += (
'\n'
+ output_indent
+ '########################################################################################################################'
)
python_code += '\n' + output_indent + f'### End conversion from: {file_name}'
python_code += (
'\n'
+ output_indent
+ '########################################################################################################################'
)
return python_code
def generate(target: str):
if not (target == 'android' or target == 'fuchsia'):
exit('Target must be android or fuchsia')
output_file_name = 'generate_%s_build.py' % target
print('Writing to: ' + output_file_name)
meson_options = process_meson('meson_options.txt')
meson_build = process_meson('meson.build')
content = generator_template.render(
meson_options=meson_options,
meson_build=meson_build,
)
with open(output_file_name, 'w') as file:
file.write(content)
def usage():
print('Usage: -t [android|fuchsia]')
sys.exit()
def main(argv):
target = 'android'
try:
opts, args = getopt.getopt(
argv,
'ht:',
[
'help',
'target=',
],
)
for opt, arg in opts:
if opt in ('-h', '--help'):
usage()
elif opt in ('-t', '--target'):
target = arg
except getopt.GetoptError as _:
usage()
generate(target)
if __name__ == '__main__':
main(sys.argv[1:])

11
meson_to_hermetic/lint.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash
if [ ! -d "venv" ]; then
echo "A venv folder was not found for this project, try running setup-venv.sh!"
exit 0
fi
source venv/bin/activate
# automatically fixes formatting that isn't considered 'unsafe'
ruff format --config "format.quote-style = 'single'"
ruff check # This is for unsafe fixes
echo "The above errors displays fixes that must be fixed manually."

View file

@ -0,0 +1,343 @@
import re
import sys
from lark import Lark, Tree
from lark.visitors import Interpreter
# This grammar derived from:
# https://mesonbuild.com/Syntax.html#grammar
meson_grammar = r"""
?start: (statement | COMMENT | NEWLINE)*
?additive_expression: multiplicative_expression | (additive_expression additive_operator multiplicative_expression)
additive_operator: PLUS | MINUS
argument_list: positional_arguments [COMMA keyword_arguments] [COMMA] | keyword_arguments
array_literal: LBRACKET [expression_list] RBRACKET
?assignment_statement: assignment_expression
assignment_expression: expression assignment_operator expression
assignment_operator: EQUALS | PLUS_EQUALS
binary_literal: "0b" BINARY_NUMBER
BINARY_NUMBER: /[01]+/
boolean_literal: TRUE | FALSE
build_definition: (NEWLINE | statement)*
condition: expression
?conditional_expression: logical_or_expression | (logical_or_expression "?" expression ":" expression)
decimal_literal: DECIMAL_NUMBER
DECIMAL_NUMBER: /[0-9][0-9]*/
dictionary_literal: LBRACE [key_value_list] RBRACE
?equality_expression: relational_expression | (equality_expression equality_operator relational_expression)
equality_operator: DOUBLE_EQUAL | NOT_EQUAL
?expression: conditional_expression | logical_or_expression
expression_list: expression (COMMA expression)* COMMA?
?expression_statement: expression
?function_expression: id_expression LPAREN [argument_list] RPAREN
hex_literal: "0x" HEX_NUMBER
HEX_NUMBER: /[a-fA-F0-9]+/
id_expression: IDENTIFIER
IDENTIFIER: /[a-zA-Z_][a-zA-Z_0-9]*/
identifier_list: id_expression (COMMA id_expression)*
integer_literal: decimal_literal | octal_literal | hex_literal
iteration_statement: FOREACH identifier_list COLON expression NEWLINE (statement | jump_statement)* ENDFOREACH
jump_statement: (BREAK | CONTINUE) NEWLINE
key_value_item: expression COLON expression
key_value_list: key_value_item (COMMA key_value_item)* COMMA?
keyword_item: id_expression ":" expression
keyword_arguments: keyword_item (COMMA keyword_item)* COMMA?
?literal: integer_literal | string_literal | boolean_literal | array_literal | dictionary_literal
?logical_and_expression: equality_expression | (logical_and_expression AND ["\\"] equality_expression)
?logical_or_expression: logical_and_expression | (logical_or_expression OR ["\\"] logical_and_expression)
?method_expression: postfix_expression ["\\"] DOT function_expression
?multiplicative_expression: unary_expression | (multiplicative_expression multiplicative_operator unary_expression)
multiplicative_operator: ASTERISK | SLASH | PERCENT
octal_literal: "0o" OCTAL_NUMBER
OCTAL_NUMBER: /[0-7]+/
positional_arguments: expression (COMMA expression)*
postfix_expression: primary_expression | subscript_expression | function_expression | method_expression
?primary_expression: literal | (LPAREN expression RPAREN) | id_expression
?relational_expression: additive_expression | (relational_expression relational_operator additive_expression)
relational_operator: GREATER | LESSTHAN | GREATER_OR_EQUAL | LESSTHAN_OR_EQUAL | IN | (NOT IN)
selection_statement: IF condition NEWLINE (statement)* (ELIF condition NEWLINE (statement)*)* [ELSE NEWLINE (statement)*] ENDIF
statement: (expression_statement | selection_statement | iteration_statement | assignment_statement) NEWLINE
string_literal: STRING_SIMPLE_VALUE | STRING_MULTILINE_VALUE
?subscript_expression: postfix_expression LBRACKET expression RBRACKET
?unary_expression: postfix_expression | (unary_operator unary_expression)
unary_operator: NOT | DASH
AND: /and/
ASTERISK: /\*/
BREAK: /break/
CONTINUE: /continue/
COLON: /:/
COMMA: /,/
DASH: /-/
DOT: /\./
DOUBLE_EQUAL: /==/
EQUALS: /=/
FOREACH: /foreach/
GREATER: />/
GREATER_OR_EQUAL: />=/
# Raise priorities to avoid elif parsed as a statement
ELIF.1: /elif/
ELSE.1: /else/
ENDIF.1: /endif/
ENDFOREACH: /endforeach/
FALSE: /false/
IF: /if /
IN: / in /
LBRACKET: /\[/
NOT: /not /
NOT_EQUAL: /!=/
RBRACKET: /\]/
LESSTHAN: /</
LESSTHAN_OR_EQUAL: /<=/
LBRACE: /{/
LPAREN: /\(/
RBRACE: /}/
RPAREN: /\)/
OR: /or/
PERCENT: /%/
PLUS: /\+/
MINUS: /-/
PLUS_EQUALS: /\+=/
NEWLINE: ( / *\r?\n/ | COMMENT )+
COMMENT: / *\#.*\n/
SLASH: /\//
STRING_SIMPLE_VALUE: /f?'(.*\\')*.*?'/
STRING_MULTILINE_VALUE: /f?'''.*?'''/s
TRUE: /true/
%import common.WS
%ignore WS
# Comments would be nice to keep, but parsing fails end-of-line comments
%ignore COMMENT
"""
class TreeToCode(Interpreter):
indent = ''
def statement(self, tree):
string = ''
for child in tree.children:
if isinstance(child, Tree):
string += self.visit(child)
elif child is not None:
string += child
return self.indent + string
def more_indent(self):
self.indent += ' '
def less_indent(self):
self.indent = self.indent[0 : len(self.indent) - 2]
# Ensure spaces around 'and'
def logical_and_expression(self, tree):
assert len(tree.children) == 3
lhs = self.visit(tree.children[0])
rhs = self.visit(tree.children[2])
return lhs + ' and ' + rhs
# Ensure spaces around 'or'
def logical_or_expression(self, tree):
assert len(tree.children) == 3
lhs = self.visit(tree.children[0])
rhs = self.visit(tree.children[2])
return lhs + ' or ' + rhs
# A ? B : C becomes B if A else C
def conditional_expression(self, tree):
assert len(tree.children) == 3
expr = self.visit(tree.children[0])
first = self.visit(tree.children[1])
second = self.visit(tree.children[2])
return first + ' if ' + expr + ' else ' + second
def assignment_expression(self, tree):
assert len(tree.children) == 3
lhs = self.visit(tree.children[0])
operator = self.visit(tree.children[1])
rhs = self.visit(tree.children[2])
if operator == '+=' and rhs.startswith('{'):
# Convert += to |= for dictionaries
return lhs + ' |= ' + rhs
elif operator == '+=' and rhs.startswith("'"):
# Handle literal string append to list or string
return (
lhs
+ ' += '
+ '['
+ rhs
+ '] if isinstance('
+ lhs
+ ', list) else '
+ rhs
)
return lhs + operator + rhs
def iteration_statement(self, tree):
# foreach = tree.children[0]
identifier_list = self.visit(tree.children[1])
# colon = tree.children[2]
id_expression = self.visit(tree.children[3])
# newline = tree.children[4]
string = 'for ' + identifier_list + ' in ' + id_expression
string += (
'.items():\n' if re.search(r',', identifier_list) is not None else ':\n'
)
self.more_indent()
lastindex = len(tree.children) - 1
for child in tree.children[5:lastindex]:
if isinstance(child, Tree):
string += self.visit(child)
elif child is not None:
string += child
self.less_indent()
return string
def selection_statement(self, tree):
string = ''
index = 0
while index < len(tree.children):
prefix = tree.children[index]
index = index + 1
if prefix is None:
continue
if isinstance(prefix, Tree):
exit('unexpected prefix: ' + prefix.pretty())
if re.match(r' *endif', prefix) is not None:
break
if re.match(r'if', prefix) is not None:
condition = self.visit(tree.children[index])
index += 1
# Skip indent here because all statements are prepended with the indentation
string += 'if ' + condition + ':\n'
elif re.match(r'elif', prefix) is not None:
condition = self.visit(tree.children[index])
index = index + 1
string += self.indent + 'elif ' + condition + ':\n'
elif re.match(r'else', prefix) is not None:
string += self.indent + 'else:\n'
else:
exit('Not a prefix: ' + prefix)
# newline = tree.children[index]
index += 1
statement_count = 0
self.more_indent()
while index < len(tree.children):
statement = tree.children[index]
if not isinstance(statement, Tree):
break
string += self.visit(statement)
index = index + 1
statement_count = statement_count + 1
if statement_count == 0:
string += self.indent + 'noop()\n'
self.less_indent()
return string
def postfix_expression(self, tree):
string = ''
for child in tree.children:
if isinstance(child, Tree):
subtree = self.visit(child)
subtree = re.sub(r'(.+)\.to_int\(\)', r'int(\g<1>)', subtree)
subtree = re.sub(r'(.+)\.to_string\(\)', r'str(\g<1>)', subtree)
subtree = re.sub(r'(.+)\.length\(\)', r'len(\g<1>)', subtree)
subtree = re.sub(r'(.+)\.to_upper\(\)', r'\g<1>.upper()', subtree)
subtree = re.sub(
r'(.+)\.underscorify\(\)',
r"\g<1>.replace('.', '_').replace('/', '_')",
subtree,
)
string += subtree
elif child is not None:
string += child
return string
def function_expression(self, tree):
assert len(tree.children) == 4
identifier = self.visit(tree.children[0])
if identifier == 'import':
identifier = 'module_import'
lparen = tree.children[1]
args = (
self.visit(tree.children[2]) if isinstance(tree.children[2], Tree) else ''
)
rparen = tree.children[3]
if identifier == 'contains':
return 'count' + lparen + args + rparen + ' > 0'
return identifier + lparen + args + rparen
def multiplicative_expression(self, tree):
assert len(tree.children) == 3
lhs = self.visit(tree.children[0])
operator = self.visit(tree.children[1])
rhs = self.visit(tree.children[2])
# Slash used mostly to concatenate strings
if operator == '/':
return (
'('
+ lhs
+ ' + '
+ rhs
+ ') if isinstance('
+ lhs
+ ', str) else ('
+ lhs
+ ' / '
+ rhs
+ ')'
)
return lhs + operator + rhs
# Switch from colon to equals
def keyword_item(self, tree):
id_ = self.visit(tree.children[0])
args = self.visit(tree.children[1])
return id_ + '=' + args
def boolean_literal(self, tree):
assert len(tree.children) == 1
value = tree.children[0]
if value == 'true':
return 'True'
elif value == 'false':
return 'False'
exit('Unhandled value: ' + value)
def string_literal(self, tree):
assert len(tree.children) == 1
string = tree.children[0]
string = re.sub(r'(@[0-9]@)', r'{}', string)
if string.startswith('f'):
string = re.sub(r'(@(.+)@)', r'{\g<2>}', string)
return string
def __default__(self, tree):
string = ''
for child in tree.children:
if isinstance(child, Tree):
string += self.visit(child)
elif child is not None:
string += child
return string
# Converts the given file from meson to python and returns the content as a string
def meson2python(file_name):
meson_parser = Lark(meson_grammar, parser='earley')
with open(file_name) as f:
# Ensure newline before end of file
tree = meson_parser.parse(f.read() + '\n')
code = TreeToCode().visit(tree)
return code
if __name__ == '__main__':
meson2python(sys.argv[1])

View file

@ -0,0 +1,333 @@
import os
import warnings
import meson_impl as impl
_gArrayOptions = []
_gFeatureOptions = []
_gBooleanOptions = []
_gComboOptions = []
_gSimpleOptions = []
def noop():
return
def message(str):
print(str)
def error(message):
exit(message)
def warning(message):
warnings.warn(message)
def set_relative_dir(dir):
impl.set_relative_dir(dir)
def files(*filenames):
file_list = []
for file in filenames:
file_list.append(impl.File(os.path.join(impl.get_relative_dir(), file)))
return file_list
def declare_dependency(
compile_args=[],
d_import_dirs=[],
d_module_versions='',
dependencies=[],
extra_files=[],
include_directories=[],
link_args=[],
link_whole=[],
link_with=[],
objects=[],
sources=[],
variables=[],
version='',
):
link_with = impl.get_linear_list([link_with])
link_whole = impl.get_linear_list([link_whole])
return impl.Dependency(
'declared',
version,
found=True,
compile_args=compile_args,
include_directories=include_directories,
dependencies=dependencies,
sources=sources,
link_with=link_with,
link_whole=link_whole,
)
def find_program(name: str, required=False, native=False, disabler=False, version=''):
if type(required) is impl.FeatureOption:
required = required.state == impl.EnableState.ENABLED
if type(required) is not bool:
exit('Unhandled required type: ' + str(type(required)))
maybe_filename = impl.get_relative_dir(name)
# may be a script in the current directory
if os.path.isfile(maybe_filename):
return impl.Program(maybe_filename, found=True)
# These are required for building turnip though not tagged as such
if name == 'bison' or name == 'flex' or name == 'gzip':
return impl.Program(name, found=True)
if (
name == 'byacc'
or name == 'glslangValidator'
or name == 'install_megadrivers.py'
or name == 'nm'
or name == 'python'
or name == 'symbols-check.py'
or name == 'sphinx-build'
):
return impl.Program(name, found=required)
exit('Unhandled program check: ' + name)
def add_project_arguments(args, language=[], native=False):
impl.add_project_arguments(args, language, native)
def add_project_link_arguments(args, language=[], native=False):
return
# Used by meson_options.txt to define an option
def option(
name: str,
type: str,
min: int = 0,
max: int = 0,
value='',
choices=[],
description='',
deprecated=None,
):
if type == 'array':
global _gArrayOptions
_gArrayOptions.append(impl.ArrayOption(name, value))
return
if type == 'feature':
global _gFeatureOptions
if value == '' or value == 'auto':
state = impl.EnableState.AUTO
elif value == 'disabled':
state = impl.EnableState.DISABLED
elif value == 'enabled':
state = impl.EnableState.ENABLED
else:
exit('Unhandled feature option value')
_gFeatureOptions.append(impl.FeatureOption(name, state))
return
if type == 'boolean':
global _gBooleanOptions
if isinstance(value, str):
flag = True if value.lower() == 'true' else False
_gBooleanOptions.append(impl.BooleanOption(name, flag))
else:
_gBooleanOptions.append(impl.BooleanOption(name, value))
return
if type == 'combo':
global _gComboOptions
_gComboOptions.append(impl.ComboOption(name, value))
return
if type == 'string' or type == 'integer':
global _gSimpleOptions
_gSimpleOptions.append(impl.SimpleOption(name, value))
return
def set_option(name, value: str):
print('set_option: %s=%s' % (name, value))
for option in _gArrayOptions:
if option.name == name:
option.set(value)
for option in _gFeatureOptions:
if option.name == name:
option.set(value)
for option in _gBooleanOptions:
if option.name == name:
option.set(value)
for option in _gComboOptions:
if option.name == name:
option.set(value)
for option in _gSimpleOptions:
if option.name == name:
option.set(value)
for option in impl.get_project_options():
if option.name == name:
option.set(value)
def get_option(name):
for option in _gArrayOptions:
if option.name == name:
return option.strings
for option in _gFeatureOptions:
if option.name == name:
return option
for option in _gBooleanOptions:
if option.name == name:
return option.value
for option in _gComboOptions:
if option.name == name:
return option.value
for option in _gSimpleOptions:
if option.name == name:
return option.value
for option in impl.get_project_options():
if option.name == name:
return option.value
# built-in options
if name == 'layout':
return 'mirror'
if name == 'prefix':
return 'prefix'
if name == 'libdir':
return 'libdir'
if name == 'datadir':
return 'datadir'
if name == 'sysconfdir':
return 'sysconfdir'
if name == 'includedir':
return 'includedir'
if name == 'c_args':
return ''
if name == 'cpp_rtti':
return False
if name == 'debug':
return True
if name == 'b_sanitize':
return False
if name == 'backend':
return 'custom'
exit('Unhandled option: ' + name)
def project(name, language_list, version, license, meson_version, default_options=[]):
impl.project(name, language_list, version, license, meson_version, default_options)
def run_command(program, *commands, check=False):
return program.run_command(commands)
def environment():
return impl.Environment()
def join_paths(*paths):
joined_path = ''
for path in paths:
joined_path = os.path.join(joined_path, path)
return joined_path
def executable(
target_name,
*source,
c_args=[],
cpp_args=[],
c_pch='',
build_by_default=False,
build_rpath='',
d_debug=[],
d_import_dirs=[],
d_module_versions=[],
d_unittest=False,
dependencies=[],
export_dynamic=False,
extra_files='',
gnu_symbol_visibility='',
gui_app=False,
implib=False,
implicit_include_directories=False,
include_directories=[],
install=False,
install_dir='',
install_mode=[],
install_rpath='',
install_tag='',
link_args=[],
link_depends='',
link_language='',
link_whole=[],
link_with=[],
name_prefix='',
name_suffix='',
native=False,
objects=[],
override_options=[],
pie=False,
rust_crate_type='',
rust_dependency_map={},
sources='',
vala_args=[],
vs_module_defs='',
win_subsystem='',
):
return impl.Executable(target_name)
def test(
name,
executable,
args=[],
depends=[],
env=[],
is_parallel=False,
priority=0,
protocol='',
should_fail=False,
suite='',
timeout=0,
verbose=False,
workdir='',
):
return
def summary(entry, bool_yn=False, list_sep='', section=''):
return
def install_headers(*headers, subdir=''):
return
def install_data(
*files,
follow_symlinks=False,
install_dir='',
install_mode=[],
install_tag='',
preserve_path=False,
rename=[],
sources=[],
):
return

View file

@ -0,0 +1,802 @@
from enum import Enum
from abc import ABC, abstractmethod
import os
import re
import subprocess
import tomllib
# The file used to write output build definitions.
_gOutputFile = ''
# The relative directory that is currently being processed. When files are
# referenced they are relative to this path.
_gRelativeDir = ''
# Global compiler flags
_gProjectCflags = []
_gProjectCppflags = []
# Parameters set by config file
_gCpuFamily = 'unknown'
_gCpu = _gCpuFamily
_gProjectVersion = 'unknown'
_gProjectOptions = []
# Caches the list of dependencies found in .toml config files
# Structure:
# DependencyTargetType
# SHARED_LIBRARY = 1
# STATIC_LIBRARY = 2
# HEADER_LIBRARY = 3
# See meson_impl.py
# external_dep = {
# 'zlib': {
# # target_name: target_type
# 'libz': 2
# },
# }
external_dep = {}
class IncludeDirectories:
def __init__(self, name: str, dirs: []):
self.name = name
self.dirs = dirs
def __iter__(self):
return iter([self])
class File:
def __init__(self, name: str):
self.name = name
class Machine:
def __init__(self, system):
self._system = system
def system(self):
return self._system
def set_system(self, system: str):
self._system = system
def cpu_family(self):
return _gCpuFamily
def cpu(self):
return _gCpuFamily
class DependencyTargetType(Enum):
SHARED_LIBRARY = 1
STATIC_LIBRARY = 2
HEADER_LIBRARY = 3
class DependencyTarget:
def __init__(self, target_name: str, target_type: DependencyTargetType):
self.target_name = target_name
self.target_type = target_type
class Dependency:
_id_generator = 1000
def __init__(
self,
name: str,
version='',
found=False,
targets=[],
compile_args=[],
include_directories=[],
dependencies=[],
sources=[],
link_with=[],
link_whole=[],
):
self.name = name
self.targets = targets
self._version = version
self._found = found
self.compile_args = compile_args
self.include_directories = include_directories
self.dependencies = dependencies
self.sources = sources
self.link_with = link_with
self.link_whole = link_whole
Dependency._id_generator += 1
self.unique_id = Dependency._id_generator
def version(self):
return self._version
def found(self):
return self._found
def partial_dependency(self, compile_args=''):
return self
def __iter__(self):
return iter([self])
def __hash__(self):
return hash(self.unique_id)
def __eq__(self, other):
return self.unique_id == other._unique_id
class CommandReturn:
def __init__(self, completed_process):
self.completed_process = completed_process
def returncode(self):
return self.completed_process.returncode
def stdout(self):
return self.completed_process.stdout
class Program:
def __init__(self, command, found: bool):
self.command = command
self._found = found
# Running commands from the ambient system may give wrong/misleading results, since
# some build systems use hermetic installations of tools like python.
def run_command(self, *commands, capture_output=False):
command_line = [self.command]
for command in commands:
command_line += command
completed_process = subprocess.run(
command_line, check=False, capture_output=capture_output
)
return CommandReturn(completed_process)
def found(self):
return self._found
def full_path(self):
return 'full_path'
class PythonModule:
def find_installation(self, name: str):
if name == 'python3':
return Program(name, found=True)
exit('Unhandled python installation: ' + name)
class EnableState(Enum):
ENABLED = 1
DISABLED = 2
AUTO = 3
class FeatureOption:
def __init__(self, name, state=EnableState.AUTO):
self.name = name
self.state = state
def allowed(self):
return self.state == EnableState.ENABLED or self.state == EnableState.AUTO
def enabled(self):
return self.state == EnableState.ENABLED
def disabled(self):
return self.state == EnableState.DISABLED
def disable_auto_if(self, value: bool):
if value and self.state == EnableState.AUTO:
self.state = EnableState.DISABLED
return self
def disable_if(self, value: bool, error_message: str):
if not value:
return self
if self.state == EnableState.ENABLED:
exit(error_message)
return FeatureOption(self.name, state=EnableState.DISABLED)
def require(self, value: bool, error_message: str):
if value:
return self
if self.state == EnableState.ENABLED:
exit(error_message)
return FeatureOption(self.name, state=EnableState.DISABLED)
def set(self, value: str):
value = value.lower()
if value == 'auto':
self.state = EnableState.AUTO
elif value == 'enabled':
self.state = EnableState.ENABLED
elif value == 'disabled':
self.state = EnableState.DISABLED
else:
exit('Unable to set feature to: %s' % value)
class ArrayOption:
def __init__(self, name: str, value: []):
self.name = name
self.strings = value
def set(self, value: str):
if value == '':
self.strings = []
else:
self.strings = [value]
class ComboOption:
def __init__(self, name: str, value: str):
self.name = name
self.value = value
def set(self, value: str):
self.value = value
class BooleanOption:
def __init__(self, name, value: bool):
assert type(value) is bool
self.name = name
self.value = value
def set(self, value: str):
self.value = bool(value)
# Value can be string or other type
class SimpleOption:
def __init__(self, name, value):
self.name = name
self.value = value
def set(self, value: str):
if type(self.value) is int:
self.value = int(value)
else:
self.value = value
class Environment:
def set(self, var, val):
return
def append(self, var, val):
return
class StaticLibrary:
def __init__(self, target_name, link_with=[], link_whole=[]):
self.target_name = target_name
self.link_with = link_with
self.link_whole = link_whole
class SharedLibrary:
name = ''
def __init__(self, name):
self.name = name
class Executable:
def __init__(self, name):
self.name = name
class CustomTargetItem:
def __init__(self, custom_target, index):
self.target = custom_target
self.index = index
class CustomTarget:
def __init__(self, name, outputs=[], generates_h=False, generates_c=False):
self._name = name
self._outputs = outputs
self._generates_h = generates_h
self._generates_c = generates_c
@property
def outputs(self):
return self._outputs
def generates_h(self):
return self._generates_h
def generates_c(self):
return self._generates_c
def target_name(self):
return self._name
def target_name_h(self):
if self._generates_h and self._generates_c:
return self._name + '.h'
return self._name
def target_name_c(self):
if self._generates_h and self._generates_c:
return self._name + '.c'
return self._name
def header_outputs(self):
hdrs = []
for out in self._outputs:
if out.endswith('.h'):
hdrs.append(out)
return hdrs
def __iter__(self):
return iter([self])
def __getitem__(self, index):
return CustomTargetItem(self, index)
def full_path(self):
return 'fullpath'
class Meson:
def __init__(self, compiler):
self._compiler = compiler
def get_compiler(self, language_string, native=False):
return self._compiler
def set_compiler(self, compiler):
self._compiler = compiler
def project_version(self):
return _gProjectVersion
def project_source_root(self):
return os.getcwd()
def is_cross_build(self):
return True
def can_run_host_binaries(self):
return False
def current_source_dir(self):
return os.getcwd()
def current_build_dir(self):
return '@CURRENT_BUILD_DIR@'
def project_build_root(self):
return '@PROJECT_BUILD_ROOT@'
def add_devenv(self, env):
return
class Compiler(ABC):
def __init__(self):
self._id = 'clang'
@abstractmethod
def has_header_symbol(
self,
header: str,
symbol: str,
args=None,
dependencies=None,
include_directories=None,
no_builtin_args: bool = False,
prefix=None,
required: bool = False,
) -> bool:
pass
@abstractmethod
def check_header(self, header: str, prefix: str = '') -> bool:
pass
@abstractmethod
def has_function(self, function, args=None, prefix='', dependencies='') -> bool:
pass
@abstractmethod
def links(self, snippet: str, name: str, args=None, dependencies=None) -> bool:
pass
def get_id(self):
return self._id
def is_symbol_supported(self, header: str, symbol: str):
if header == 'sys/mkdev.h' or symbol == 'program_invocation_name':
return False
return True
def is_function_supported(self, function: str):
if (
function == 'qsort_s'
or function == 'pthread_setaffinity_np'
or function == 'secure_getenv'
):
return False
return True
def is_link_supported(self, name: str):
if name == 'GNU qsort_r' or name == 'BSD qsort_r':
return False
return True
def is_header_supported(self, header: str):
if (
header == 'xlocale.h'
or header == 'pthread_np.h'
or header == 'renderdoc_app.h'
):
return False
return True
def get_define(self, define: str, prefix: str):
if define == 'ETIME':
return define
exit('Unhandled define: ' + define)
def get_supported_function_attributes(self, attributes: list[str]):
# Assume all are supported
return attributes
def has_function_attribute(self, attribute: str):
return True
def has_argument(self, name: str):
result = True
print("has_argument '%s': %s" % (name, str(result)))
return result
def has_link_argument(self, name: str):
result = True
print("has_link_argument '%s': %s" % (name, str(result)))
return result
def compiles(self, snippet, name: str):
# Exclude what is currently not working.
result = True
if name == '__uint128_t':
result = False
print("compiles '%s': %s" % (name, str(result)))
return result
def has_member(self, struct, member, prefix):
# Assume it does
return True
def get_argument_syntax(self):
return 'gcc'
def get_supported_arguments(self, args):
supported_args = []
for arg in args:
if (
arg.startswith('-flifetime-dse')
or arg.startswith('-Wno-format-truncation')
or arg.startswith('-Wno-nonnull-compare')
or arg.startswith('-Wno-class-memaccess')
or arg.startswith('-Wno-format-truncation')
):
continue
supported_args.append(arg)
return supported_args
def get_supported_link_arguments(self, args):
return args
def find_library(self, name, required=False):
if name == 'ws2_32' or name == 'elf' or name == 'm' or name == 'sensors':
return Dependency(name, found=required)
exit('Unhandled library: ' + name)
def sizeof(self, string):
table = _get_sizeof_table()
if string not in table:
exit('Unhandled compiler sizeof: ' + string)
return table[string]
class PkgConfigModule:
def generate(self, lib, name='', description='', extra_cflags=None):
pass
###################################################################################################
def fprint(args):
print(args, file=_gOutputFile)
def set_relative_dir(dir):
global _gRelativeDir
_gRelativeDir = dir
def open_output_file(name):
global _gOutputFile
_gOutputFile = open(name, 'w')
def close_output_file():
global _gOutputFile
_gOutputFile.close()
def get_relative_dir(path_or_file=''):
if isinstance(path_or_file, File):
return path_or_file.name
assert isinstance(path_or_file, str)
if path_or_file == '':
return _gRelativeDir
return os.path.join(_gRelativeDir, path_or_file)
def get_relative_gen_dir(path=''):
return os.path.join(_gRelativeDir, path)
def project(name, language_list, version, license, meson_version, default_options):
if type(version) is str:
_gProjectVersion = version
else:
assert type(version) is list
version_file = version[0]
assert type(version_file) is File
with open(version_file.name, 'r') as file:
for line in file:
_gProjectVersion = line.strip()
break
for option in default_options:
value_pair = option.split('=')
_gProjectOptions.append(SimpleOption(value_pair[0], value_pair[1]))
def get_project_options():
return _gProjectOptions
def load_config_file(filename):
if not filename.endswith('.toml'):
exit('Config file that is not .toml is not supported.')
with open(filename, 'rb') as f:
data = tomllib.load(f)
project_configs = data.get('project_config')
if project_configs is None:
exit(f'meson_options not defined in {filename}.')
project_config = project_configs[0]
# TODO(bpnguyen): Make so project config isn't hardcoded to pick the first set of configs
host_machine_settings = project_config.get('host_machine')
for key in host_machine_settings:
match key:
case 'cpu_family':
cpu_fam = host_machine_settings.get(key)
global _gCpuFamily
_gCpuFamily = cpu_fam
print(f'Config: cpu_family={_gCpuFamily}')
case 'cpu':
cpu = host_machine_settings.get(key)
global _gCpu
_gCpu = cpu
print(f'Config: cpu={_gCpu}')
case 'host_machine' | 'build_machine':
continue
case _: # Default case
exit(f'Unhandled config key: {key}')
def add_project_arguments(args, language=[], native=False):
global _gProjectCflags, _gProjectCppflags
if type(args) is not list:
args = [args]
for lang in language:
for arg in args:
if isinstance(arg, list):
add_project_arguments(arg, language=language, native=native)
continue
assert isinstance(arg, str)
if lang == 'c':
print('cflags: ' + arg)
_gProjectCflags.append(arg)
elif lang == 'cpp':
print('cppflags: ' + arg)
_gProjectCppflags.append(arg)
else:
exit('Unhandle arguments language: ' + lang)
def get_project_cflags():
return _gProjectCflags
def get_project_cppflags():
return _gProjectCppflags
def _get_sizeof_table():
table_32 = {'void*': 4}
table_64 = {'void*': 8}
if _gCpuFamily == 'arm':
table = table_32
elif _gCpuFamily == 'aarch64':
table = table_64
else:
exit('sizeof unhandled cpu family: %s' % _gCpuFamily)
return table
def get_linear_list(arg_list):
args = []
for arg in arg_list:
if type(arg) is list:
args.extend(get_linear_list(arg))
else:
args.append(arg)
return args
def load_dependencies(config):
with open(config, 'rb') as f:
data = tomllib.load(f)
project_configs = data.get('project_config')
for project_config in project_configs:
dependencies = project_config.get('ext_dependencies')
for dep_name, targets in dependencies.items():
dep_targets = {
t.get('target_name'): t.get('target_type') for t in targets
}
external_dep[dep_name] = dep_targets
def dependency(*names, required=True, version=''):
for name in names:
print('dependency: %s' % name)
if name == '':
return Dependency('null', version, found=False)
if name in external_dep:
targets = external_dep.get(name)
return Dependency(
name,
targets=[
DependencyTarget(t, DependencyTargetType(targets[t]))
for t in targets
],
version=version,
found=True,
)
if (
name == 'backtrace'
or name == 'curses'
or name == 'expat'
or name == 'libconfig'
or name == 'libmagma_virt'
or name == 'libva'
or name == 'libzstd'
or name == 'libdrm'
or name == 'libglvnd'
or name == 'libudev'
or name == 'libunwind'
or name == 'llvm'
or name == 'libxml-2.0'
or name == 'lua54'
or name == 'valgrind'
or name == 'wayland-scanner'
):
return Dependency(name, version, found=False)
if (
name == 'libarchive'
or name == 'libelf'
or name == 'threads'
or name == 'vdpau'
):
return Dependency(name, version, found=required)
exit('Unhandled dependency: ' + name)
def get_set_of_deps(deps, set_of_deps=set()):
for dep in deps:
if type(dep) is list:
set_of_deps = get_set_of_deps(dep, set_of_deps)
elif dep not in set_of_deps:
set_of_deps.add(dep)
set_of_deps = get_set_of_deps(dep.dependencies, set_of_deps)
return set_of_deps
def get_include_dirs(paths) -> list[str]:
dir_list = []
for path in paths:
if type(path) is list:
dir_list.extend(get_include_dirs(p for p in path))
elif type(path) is IncludeDirectories:
dir_list.extend(path.dirs)
else:
assert type(path) is str
dir_list.append(get_relative_dir(path))
return dir_list
def get_include_directories(includes) -> list[IncludeDirectories]:
dirs = []
if type(includes) is list:
for inc in includes:
dirs.extend(get_include_directories(inc))
elif type(includes) is IncludeDirectories:
dirs.extend(includes)
else:
assert type(includes) is str
exit('get_include_directories got string: %s' % includes)
return dirs
def get_static_libs(arg_list):
libs = []
for arg in arg_list:
if type(arg) is list:
libs.extend(get_static_libs(arg))
else:
assert type(arg) is StaticLibrary
libs.extend(get_static_libs(arg.link_with))
libs.append(arg)
return libs
def get_whole_static_libs(arg_list):
libs = []
for arg in arg_list:
if type(arg) is list:
libs.extend(get_whole_static_libs(arg))
else:
assert type(arg) is StaticLibrary
libs.extend(get_whole_static_libs(arg._link_whole))
libs.append(arg)
return libs
def get_list_of_relative_inputs(list_or_string):
if isinstance(list_or_string, list):
ret = []
for item in list_or_string:
ret.extend(get_list_of_relative_inputs(item))
return ret
return [get_relative_dir(list_or_string)]
def get_command_line_from_args(args: list):
command_line = ''
for arg in args:
command_line += ' ' + arg
# Escape angle brackets
command_line = re.sub(r'(<|>)', '\\\\\\\\\g<1>', command_line)
return command_line
def replace_wrapped_input_with_target(args, python_script, python_script_target_name):
outargs = []
for index, arg in enumerate(args):
pattern = '(.*?)(' + python_script + ')'
replace = '\g<1>' + python_script_target_name
outargs.append(re.sub(pattern, replace, arg))
return outargs

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
lark>=1.1.9
ruff>=0.5.4
Mako>=1.3.5
Jinja2>=3.1.4

16
meson_to_hermetic/setup-venv.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
if [ -d "venv" ]; then
echo "A venv folder already exists in this project!"
exit 0
fi
if command -v python3 >/dev/null 2>&1; then
echo "Currently creating a python virtual environment..."
python3 -m venv venv
source venv/bin/activate
echo "Done creating new venv folder and terminal is now using venv."
echo "Now attempting to install dependencies..."
pip install -r requirements.txt
echo "Successfully finished installing dependencies!"
else
echo "Python 3 is not currently installed on your machine!"
fi

View file

@ -0,0 +1,63 @@
import sys
import tomllib
###
########################################################################################################################
###
### Pull in the definitions meson is expecting
###
from meson_common import *
from meson_to_hermetic import *
###
########################################################################################################################
{{ meson_options }}
###
### Process command line arguments for setting options
config_path = ''
if __name__ == "__main__":
for arg in sys.argv[1:]:
flag, value = arg.split('=') # args form follows a: -flag=value
match flag:
case ('--config' | '-c'):
if not value.endswith('.toml'):
exit(f'File format for {value} not supported. Please provide a valid file (toml).')
with open(value, 'rb') as f:
data = tomllib.load(f)
config_path = value
project_config = data.get('project_config')
for config in project_config:
meson_options = config.get('meson_options')
for key in meson_options:
set_option(key, meson_options[key])
case _:
exit(f'Unhandled arg={flag} with value={value}')
###
### These definitions must be inside the module
def get_variable(name: str):
return globals()[name]
### Load Metadata
load_meson_data(config_path)
meson = impl.Meson(meson_translator.generator)
host_machine = impl.Machine(meson_translator.host_machine)
build_machine = impl.Machine(meson_translator.build_machine)
### Open the build definition file
open_output_file()
### Load config
load_config_file()
### Load dependencies
load_dependencies()
{{ meson_build }}
### Close the build definition file
close_output_file()
{# Newline Placeholder #}