From 976a188837f90e545bf1cef6d1a93a99486b193c Mon Sep 17 00:00:00 2001 From: Hugo Locurcio Date: Wed, 27 Mar 2019 00:51:51 +0100 Subject: [PATCH] Fix and improve the build system - Fix automatic architecture detection - Fix compiling with MinGW on Linux - MinGW on Windows is still not working though - Default to Clang on macOS - Remove redundant `use_custom_api_file` option - Format SConstruct using Flake8 This closes #245. --- SConstruct | 161 ++++++++++++++------- binding_generator.py | 324 +++++++++++++++++++++---------------------- 2 files changed, 275 insertions(+), 210 deletions(-) diff --git a/SConstruct b/SConstruct index b9b47c30..2f4ad8e1 100644 --- a/SConstruct +++ b/SConstruct @@ -1,14 +1,16 @@ -#!python +#!/usr/bin/env python -import os, subprocess, platform, sys +import os +import sys def add_sources(sources, dir, extension): - for f in os.listdir(dir): - if f.endswith('.' + extension): - sources.append(dir + '/' + f) + for f in os.listdir(dir): + if f.endswith('.' + extension): + sources.append(dir + '/' + f) -# Try to detect the host platform automatically + +# Try to detect the host platform automatically. # This is used if no `platform` argument is passed if sys.platform.startswith('linux'): host_platform = 'linux' @@ -17,50 +19,86 @@ elif sys.platform == 'darwin': elif sys.platform == 'win32': host_platform = 'windows' else: - raise ValueError('Could not detect platform automatically, please specify with platform=') + raise ValueError( + 'Could not detect platform automatically, please specify with ' + 'platform=' + ) opts = Variables([], ARGUMENTS) - -opts.Add(EnumVariable('platform', 'Target platform', host_platform, - allowed_values=('linux', 'osx', 'windows'), - ignorecase=2)) -opts.Add(EnumVariable('bits', 'Target platform bits', 'default', ('default', '32', '64'))) -opts.Add(BoolVariable('use_llvm', 'Use the LLVM compiler - only effective when targeting Linux', False)) -opts.Add(BoolVariable('use_mingw', 'Use the MinGW compiler - only effective on Windows', False)) +opts.Add(EnumVariable( + 'platform', + 'Target platform', + host_platform, + allowed_values=('linux', 'osx', 'windows'), + ignorecase=2 +)) +opts.Add(EnumVariable( + 'bits', + 'Target platform bits', + 'default', + ('default', '32', '64') +)) +opts.Add(BoolVariable( + 'use_llvm', + 'Use the LLVM compiler - only effective when targeting Linux', + False +)) +opts.Add(BoolVariable( + 'use_mingw', + 'Use the MinGW compiler instead of MSVC - only effective on Windows', + False +)) # Must be the same setting as used for cpp_bindings -opts.Add(EnumVariable('target', 'Compilation target', 'debug', - allowed_values=('debug', 'release'), - ignorecase=2)) -opts.Add(PathVariable('headers_dir', 'Path to the directory containing Godot headers', 'godot_headers', PathVariable.PathIsDir)) -opts.Add(BoolVariable('use_custom_api_file', 'Use a custom JSON API file', False)) -opts.Add(PathVariable('custom_api_file', 'Path to the custom JSON API file', None, PathVariable.PathIsFile)) -opts.Add(BoolVariable('generate_bindings', 'Generate GDNative API bindings', False)) - -unknown = opts.UnknownVariables() -if unknown: - print("Unknown variables:" + unknown.keys()) - Exit(1) +opts.Add(EnumVariable( + 'target', + 'Compilation target', + 'debug', + allowed_values=('debug', 'release'), + ignorecase=2 +)) +opts.Add(PathVariable( + 'headers_dir', + 'Path to the directory containing Godot headers', + 'godot_headers', + PathVariable.PathIsDir +)) +opts.Add(PathVariable( + 'custom_api_file', + 'Path to a custom JSON API file', + None, + PathVariable.PathIsFile +)) +opts.Add(BoolVariable( + 'generate_bindings', + 'Generate GDNative API bindings', + False +)) env = Environment() opts.Update(env) Help(opts.GenerateHelpText(env)) -# This makes sure to keep the session environment variables on Windows -# This way, you can run SCons in a Visual Studio 2017 prompt and it will find all the required tools -if env['platform'] == 'windows': +is64 = sys.maxsize > 2**32 +if ( + env['TARGET_ARCH'] == 'amd64' or + env['TARGET_ARCH'] == 'emt64' or + env['TARGET_ARCH'] == 'x86_64' +): + is64 = True + +if env['bits'] == 'default': + env['bits'] = '64' if is64 else '32' + +# This makes sure to keep the session environment variables on Windows. +# This way, you can run SCons in a Visual Studio 2017 prompt and it will find +# all the required tools +if host_platform == 'windows': if env['bits'] == '64': env = Environment(TARGET_ARCH='amd64') elif env['bits'] == '32': env = Environment(TARGET_ARCH='x86') - else: - print("Warning: bits argument not specified, target arch is=" + env['TARGET_ARCH']) - opts.Update(env) -is64 = False -if (env['platform'] == 'osx' or env['TARGET_ARCH'] == 'amd64' or env['TARGET_ARCH'] == 'emt64' or env['TARGET_ARCH'] == 'x86_64'): - is64 = True -if env['bits'] == 'default': - env['bits'] = '64' if is64 else '32' + opts.Update(env) if env['platform'] == 'linux': if env['use_llvm']: @@ -82,11 +120,22 @@ if env['platform'] == 'linux': env.Append(LINKFLAGS=['-m32']) elif env['platform'] == 'osx': + # Use Clang on macOS by default + env['CXX'] = 'clang++' + if env['bits'] == '32': - raise ValueError('Only 64-bit builds are supported for the macOS target.') + raise ValueError( + 'Only 64-bit builds are supported for the macOS target.' + ) env.Append(CCFLAGS=['-g', '-std=c++14', '-arch', 'x86_64']) - env.Append(LINKFLAGS=['-arch', 'x86_64', '-framework', 'Cocoa', '-Wl,-undefined,dynamic_lookup']) + env.Append(LINKFLAGS=[ + '-arch', + 'x86_64', + '-framework', + 'Cocoa', + '-Wl,-undefined,dynamic_lookup', + ]) if env['target'] == 'debug': env.Append(CCFLAGS=['-Og']) @@ -101,40 +150,56 @@ elif env['platform'] == 'windows': env.Append(CCFLAGS=['/EHsc', '/D_DEBUG', '/MDd']) elif env['target'] == 'release': env.Append(CCFLAGS=['/O2', '/EHsc', '/DNDEBUG', '/MD']) - else: - # MinGW + + elif host_platform == 'linux': + # Cross-compilation using MinGW if env['bits'] == '64': env['CXX'] = 'x86_64-w64-mingw32-g++' elif env['bits'] == '32': env['CXX'] = 'i686-w64-mingw32-g++' + # Native or cross-compilation using MinGW + if host_platform == 'linux' or env['use_mingw']: env.Append(CCFLAGS=['-g', '-O3', '-std=c++14', '-Wwrite-strings']) - env.Append(LINKFLAGS=['--static', '-Wl,--no-undefined', '-static-libgcc', '-static-libstdc++']) + env.Append(LINKFLAGS=[ + '--static', + '-Wl,--no-undefined', + '-static-libgcc', + '-static-libstdc++', + ]) - -env.Append(CPPPATH=['.', env['headers_dir'], 'include', 'include/gen', 'include/core']) +env.Append(CPPPATH=[ + '.', + env['headers_dir'], + 'include', + 'include/gen', + 'include/core', +]) # Generate bindings? json_api_file = '' -if env['use_custom_api_file']: +if 'custom_api_file' in env: json_api_file = env['custom_api_file'] else: json_api_file = os.path.join(os.getcwd(), 'godot_headers', 'api.json') if env['generate_bindings']: # Actually create the bindings here - import binding_generator binding_generator.generate_bindings(json_api_file) -# source to compile +# Sources to compile sources = [] add_sources(sources, 'src/core', 'cpp') add_sources(sources, 'src/gen', 'cpp') library = env.StaticLibrary( - target='bin/' + 'libgodot-cpp.{}.{}.{}'.format(env['platform'], env['target'], env['bits']), source=sources + target='bin/' + 'libgodot-cpp.{}.{}.{}'.format( + env['platform'], + env['target'], + env['bits'], + ), source=sources ) Default(library) diff --git a/binding_generator.py b/binding_generator.py index 49d82aed..e4c64317 100644 --- a/binding_generator.py +++ b/binding_generator.py @@ -1,4 +1,4 @@ -#!python +#!/usr/bin/env python import json @@ -7,30 +7,30 @@ import json classes = [] def generate_bindings(path): - + global classes classes = json.load(open(path)) - + icalls = set() - + for c in classes: # print c['name'] used_classes = get_used_classes(c) - + header = generate_class_header(used_classes, c) - + impl = generate_class_implementation(icalls, used_classes, c) - + header_file = open("include/gen/" + strip_name(c["name"]) + ".hpp", "w+") header_file.write(header) - + source_file = open("src/gen/" + strip_name(c["name"]) + ".cpp", "w+") source_file.write(impl) - - + + icall_header_file = open("src/gen/__icalls.hpp", "w+") icall_header_file.write(generate_icall_header(icalls)) - + icall_source_file = open("src/gen/__icalls.cpp", "w+") icall_source_file.write(generate_icall_implementation(icalls)) @@ -68,12 +68,12 @@ def generate_class_header(used_classes, c): source.append("#define GODOT_CPP_" + strip_name(c["name"]).upper() + "_HPP") source.append("") source.append("") - + source.append("#include ") source.append("#include ") source.append("") - - + + source.append("#include ") class_name = strip_name(c["name"]) @@ -101,11 +101,11 @@ def generate_class_header(used_classes, c): source.append("#include \"" + used_class_name + ".hpp\"") source.append("") - + if c["base_class"] != "": source.append("#include \"" + strip_name(c["base_class"]) + ".hpp\"") - - + + source.append("namespace godot {") source.append("") @@ -115,12 +115,12 @@ def generate_class_header(used_classes, c): continue else: source.append("class " + strip_name(used_type) + ";") - - + + source.append("") - + vararg_templates = "" - + # generate the class definition here source.append("class " + class_name + (" : public _Wrapped" if c["base_class"] == "" else (" : public " + strip_name(c["base_class"])) ) + " {") @@ -168,17 +168,17 @@ def generate_class_header(used_classes, c): source.append("\t};") source.append("\n\t// constants") - + for name in c["constants"]: if name not in enum_values: source.append("\tconst static int " + name + " = " + str(c["constants"][name]) + ";") - + if c["instanciable"]: source.append("") source.append("") source.append("\tstatic " + class_name + " *_new();") - + source.append("\n\t// methods") @@ -188,28 +188,28 @@ def generate_class_header(used_classes, c): source.append("\tstatic T *cast_to(const Object *obj);") source.append("#endif") source.append("") - + for method in c["methods"]: - + method_signature = "" - + # TODO decide what to do about virtual methods # method_signature += "virtual " if method["is_virtual"] else "" method_signature += make_gdnative_type(method["return_type"]) method_name = escape_cpp(method["name"]) method_signature += method_name + "(" - - + + has_default_argument = False method_arguments = "" - + for i, argument in enumerate(method["arguments"]): method_signature += "const " + make_gdnative_type(argument["type"]) argument_name = escape_cpp(argument["name"]) method_signature += argument_name method_arguments += argument_name - - + + # default arguments def escape_default_arg(_type, default_value): if _type == "Color": @@ -236,51 +236,51 @@ def generate_class_header(used_classes, c): return "\"" + default_value + "\"" if _type == "RID": return "RID()" - + if default_value == "Null" or default_value == "[Object:null]": return "nullptr" - + return default_value - - - - + + + + if argument["has_default_value"] or has_default_argument: method_signature += " = " + escape_default_arg(argument["type"], argument["default_value"]) has_default_argument = True - - - + + + if i != len(method["arguments"]) - 1: method_signature += ", " method_arguments += "," - + if method["has_varargs"]: if len(method["arguments"]) > 0: method_signature += ", " method_arguments += ", " vararg_templates += "\ttemplate " + method_signature + "Args... args){\n\t\treturn " + method_name + "(" + method_arguments + "Array::make(args...));\n\t}\n""" method_signature += "const Array& __var_args = Array()" - + method_signature += ")" + (" const" if method["is_const"] else "") - + source.append("\t" + method_signature + ";") - + source.append(vararg_templates) source.append("};") source.append("") - - + + source.append("}") source.append("") - + source.append("#endif") - - + + return "\n".join(source) - + @@ -291,34 +291,34 @@ def generate_class_implementation(icalls, used_classes, c): source.append("#include \"" + class_name + ".hpp\"") source.append("") source.append("") - + source.append("#include ") source.append("#include ") source.append("#include ") - + source.append("#include ") source.append("") - - + + source.append("#include \"__icalls.hpp\"") source.append("") source.append("") - + for used_class in used_classes: if is_enum(used_class): continue else: source.append("#include \"" + strip_name(used_class) + ".hpp\"") - + source.append("") source.append("") - + source.append("namespace godot {") - - + + core_object_name = "this" - - + + source.append("") source.append("") @@ -326,44 +326,44 @@ def generate_class_implementation(icalls, used_classes, c): source.append("" + class_name + " *" + class_name + "::_singleton = NULL;") source.append("") source.append("") - + # FIXME Test if inlining has a huge impact on binary size source.append(class_name + "::" + class_name + "() {") source.append("\t_owner = godot::api->godot_global_get_singleton((char *) \"" + strip_name(c["name"]) + "\");") source.append("}") - + source.append("") source.append("") - - - + + + if c["instanciable"]: source.append(class_name + " *" + strip_name(c["name"]) + "::_new()") source.append("{") source.append("\treturn (" + class_name + " *) godot::nativescript_1_1_api->godot_nativescript_get_instance_binding_data(godot::_RegisterState::language_index, godot::api->godot_get_class_constructor((char *)\"" + c["name"] + "\")());") source.append("}") - + for method in c["methods"]: method_signature = "" - + method_signature += make_gdnative_type(method["return_type"]) method_signature += strip_name(c["name"]) + "::" + escape_cpp(method["name"]) + "(" - + for i, argument in enumerate(method["arguments"]): method_signature += "const " + make_gdnative_type(argument["type"]) method_signature += escape_cpp(argument["name"]) - + if i != len(method["arguments"]) - 1: method_signature += ", " - + if method["has_varargs"]: if len(method["arguments"]) > 0: method_signature += ", " method_signature += "const Array& __var_args" - + method_signature += ")" + (" const" if method["is_const"] else "") - + source.append(method_signature + " {") @@ -374,14 +374,14 @@ def generate_class_implementation(icalls, used_classes, c): source.append("") continue else: - + source.append("\tstatic godot_method_bind *mb = nullptr;") source.append("\tif (mb == nullptr) {") source.append("\t\tmb = godot::api->godot_method_bind_get_method(\"" + c["name"] +"\", \"" + method["name"] + "\");") source.append("\t}") - + return_statement = "" - + if method["return_type"] != "void": if is_class_type(method["return_type"]): if is_enum(method["return_type"]): @@ -392,72 +392,72 @@ def generate_class_implementation(icalls, used_classes, c): return_statement += "return " + ("(" + strip_name(method["return_type"]) + " *) " if is_class_type(method["return_type"]) else "") else: return_statement += "return " - + def get_icall_type_name(name): if is_enum(name): return "int" if is_class_type(name): return "Object" return name - - - + + + if method["has_varargs"]: if len(method["arguments"]) != 0: source.append("\tVariant __given_args[" + str(len(method["arguments"])) + "];") - + for i, argument in enumerate(method["arguments"]): source.append("\tgodot::api->godot_variant_new_nil((godot_variant *) &__given_args[" + str(i) + "]);") - + source.append("") - - + + for i, argument in enumerate(method["arguments"]): source.append("\t__given_args[" + str(i) + "] = " + escape_cpp(argument["name"]) + ";") - + source.append("") - + size = "" if method["has_varargs"]: size = "(__var_args.size() + " + str(len(method["arguments"])) + ")" else: size = "(" + str(len(method["arguments"])) + ")" - + source.append("\tgodot_variant **__args = (godot_variant **) alloca(sizeof(godot_variant *) * " + size + ");") - + source.append("") - + for i, argument in enumerate(method["arguments"]): source.append("\t__args[" + str(i) + "] = (godot_variant *) &__given_args[" + str(i) + "];") - + source.append("") - + if method["has_varargs"]: source.append("\tfor (int i = 0; i < __var_args.size(); i++) {") source.append("\t\t__args[i + " + str(len(method["arguments"])) + "] = (godot_variant *) &((Array &) __var_args)[i];") source.append("\t}") - + source.append("") - + source.append("\tVariant __result;") source.append("\t*(godot_variant *) &__result = godot::api->godot_method_bind_call(mb, ((const Object *) " + core_object_name + ")->_owner, (const godot_variant **) __args, " + size + ", nullptr);") - + source.append("") - + if is_class_type(method["return_type"]): source.append("\tObject *obj = Object::___get_from_variant(__result);") source.append("\tif (obj->has_method(\"reference\"))") source.append("\t\tobj->callv(\"reference\", Array());") source.append("") - - + + for i, argument in enumerate(method["arguments"]): source.append("\tgodot::api->godot_variant_destroy((godot_variant *) &__given_args[" + str(i) + "]);") - + source.append("") - + if method["return_type"] != "void": cast = "" if is_class_type(method["return_type"]): @@ -468,40 +468,40 @@ def generate_class_implementation(icalls, used_classes, c): else: cast += "__result;" source.append("\treturn " + cast) - - - + + + else: - + args = [] for arg in method["arguments"]: args.append(get_icall_type_name(arg["type"])) - + icall_ret_type = get_icall_type_name(method["return_type"]) - + icall_sig = tuple((icall_ret_type, tuple(args))) - + icalls.add(icall_sig) - + icall_name = get_icall_name(icall_sig) - + return_statement += icall_name + "(mb, (const Object *) " + core_object_name - + for arg in method["arguments"]: return_statement += ", " + escape_cpp(arg["name"]) + (".ptr()" if is_reference_type(arg["type"]) else "") - + return_statement += ")" - + source.append("\t" + return_statement + (")" if is_reference_type(method["return_type"]) else "") + ";") - + source.append("}") source.append("") - - + + source.append("}") - - + + return "\n".join(source) @@ -509,36 +509,36 @@ def generate_class_implementation(icalls, used_classes, c): def generate_icall_header(icalls): - + source = [] source.append("#ifndef GODOT_CPP__ICALLS_HPP") source.append("#define GODOT_CPP__ICALLS_HPP") - + source.append("") - + source.append("#include ") source.append("#include ") source.append("") - + source.append("#include ") source.append("#include \"Object.hpp\"") source.append("") source.append("") - + source.append("namespace godot {") source.append("") - + for icall in icalls: ret_type = icall[0] args = icall[1] - + method_signature = "" - + method_signature += return_type(ret_type) + get_icall_name(icall) + "(godot_method_bind *mb, const Object *inst" - + for arg in args: method_signature += ", const " - + if is_core_type(arg): method_signature += arg + "&" elif arg == "int": @@ -549,51 +549,51 @@ def generate_icall_header(icalls): method_signature += arg else: method_signature += "Object *" - + method_signature += ");" - + source.append(method_signature) - + source.append("") - + source.append("}") source.append("") - + source.append("#endif") - + return "\n".join(source) - + def generate_icall_implementation(icalls): source = [] source.append("#include \"__icalls.hpp\"") - + source.append("") - + source.append("#include ") source.append("#include ") source.append("") - + source.append("#include ") source.append("#include ") source.append("#include \"Object.hpp\"") source.append("") source.append("") - + source.append("namespace godot {") source.append("") - + for icall in icalls: ret_type = icall[0] args = icall[1] - + method_signature = "" - + method_signature += return_type(ret_type) + get_icall_name(icall) + "(godot_method_bind *mb, const Object *inst" - + for i, arg in enumerate(args): method_signature += ", const " - + if is_core_type(arg): method_signature += arg + "& " elif arg == "int": @@ -604,37 +604,37 @@ def generate_icall_implementation(icalls): method_signature += arg + " " else: method_signature += "Object *" - + method_signature += "arg" + str(i) - + method_signature += ")" - + source.append(method_signature + " {") - + if ret_type != "void": source.append("\t" + ("godot_object *" if is_class_type(ret_type) else return_type(ret_type)) + "ret;") if is_class_type(ret_type): source.append("\tret = nullptr;") - - + + source.append("\tconst void *args[" + ("1" if len(args) == 0 else "") + "] = {") - + for i, arg in enumerate(args): - + wrapped_argument = "\t\t" if is_primitive(arg) or is_core_type(arg): wrapped_argument += "(void *) &arg" + str(i) else: wrapped_argument += "(void *) (arg" + str(i) + ") ? arg" + str(i) + "->_owner : nullptr" - + wrapped_argument += "," source.append(wrapped_argument) - + source.append("\t};") source.append("") - + source.append("\tgodot::api->godot_method_bind_ptrcall(mb, inst->_owner, args, " + ("nullptr" if ret_type == "void" else "&ret") + ");") - + if ret_type != "void": if is_class_type(ret_type): source.append("\tif (ret) {") @@ -644,12 +644,12 @@ def generate_icall_implementation(icalls): source.append("\treturn (Object *) ret;") else: source.append("\treturn ret;") - + source.append("}") - + source.append("}") source.append("") - + return "\n".join(source) @@ -657,7 +657,7 @@ def generate_icall_implementation(icalls): def generate_type_registry(classes): source = [] - + source.append("#include \"TagDB.hpp\"") source.append("#include ") source.append("\n") @@ -719,12 +719,12 @@ def return_type(t): def get_icall_name(sig): ret_type = sig[0] args = sig[1] - + name = "___godot_icall_" name += strip_name(ret_type) for arg in args: name += "_" + strip_name(arg) - + return name @@ -737,7 +737,7 @@ def get_used_classes(c): for method in c["methods"]: if is_class_type(method["return_type"]) and not (method["return_type"] in classes): classes.append(method["return_type"]) - + for arg in method["arguments"]: if is_class_type(arg["type"]) and not (arg["type"] in classes): classes.append(arg["type"])