# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: import os, json, re, subprocess from ambuild2.frontend.v2_2.cpp.builders import TargetSuffix # Edit the functions below for the extra functionality, the return should be # a list of path's to wanted locations def additionalLibs(context, binary, sdk): return [ # Path should be relative either to hl2sdk folder or to build folder # 'path/to/lib/example.lib', ] def additionalDefines(context, binary, sdk): return [ # 'EXAMPLE_DEFINE=2' ] def additionalIncludes(context, binary, sdk): return [ # Path should be absolute only! # os.path.join(sdk['path'], 'game', 'server'), # os.path.join(sdk['path'], 'public', 'entity2'), # 'D:/absolute/path/to/include/folder/' ] def getGitHeadPath(): curr_source = builder.currentSourcePath direct_head = os.path.join(curr_source, '.git', 'HEAD') if not os.path.exists(direct_head): return (None, None) git_head_path = None with open(direct_head, 'r') as fp: head_contents = fp.read().strip() if re.search('^[a-fA-F0-9]{40}$', head_contents): git_head_path = direct_head else: git_state = head_contents.split(':')[1].strip() git_head_path = os.path.join(curr_source, '.git', git_state) if not os.path.exists(git_head_path): git_head_path = direct_head return (direct_head, git_head_path) def runAndReturn(argv): p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd = builder.currentSourcePath) output, ignored = p.communicate() rval = p.poll() if rval: raise subprocess.CalledProcessError(rval, argv) text = output.decode('utf8') return text.strip() def resolveEnvPath(env, folder = None): if env in os.environ: path = os.environ[env] if os.path.isdir(path): return path elif folder is not None: head = os.getcwd() oldhead = None while head != None and head != oldhead: path = os.path.join(head, folder) if os.path.isdir(path): return path oldhead = head head, tail = os.path.split(head) return None def resolveMMSRoot(): prenormalized_path = None if builder.options.mms_path: prenormalized_path = builder.options.mms_path else: prenormalized_path = resolveEnvPath('MMSOURCE20', 'mmsource-2.0') if not prenormalized_path: prenormalized_path = resolveEnvPath('MMSOURCE_DEV', 'metamod-source') if not prenormalized_path: prenormalized_path = resolveEnvPath('MMSOURCE_DEV', 'mmsource-central') if not prenormalized_path or not os.path.isdir(prenormalized_path): raise Exception('Could not find a source copy of Metamod:Source') return os.path.abspath(os.path.normpath(prenormalized_path)) mms_root = resolveMMSRoot() def resolveHL2SDKManifestsRoot(): prenormalized_path = None if builder.options.hl2sdk_manifests: prenormalized_path = builder.options.hl2sdk_manifests else: prenormalized_path = resolveEnvPath('HL2SDKMANIFESTS', 'hl2sdk-manifests') if not prenormalized_path or not os.path.isdir(prenormalized_path): raise Exception('Could not find a source copy of HL2SDK manifests') return os.path.abspath(os.path.normpath(prenormalized_path)) hl2sdk_manifests_root = resolveHL2SDKManifestsRoot() SdkHelpers = builder.Eval(os.path.join(hl2sdk_manifests_root, 'SdkHelpers.ambuild'), { 'Project': 'metamod' }) class MMSPluginConfig(object): def __init__(self): self.sdk_manifests = [] self.sdks = {} self.sdk_targets = [] self.binaries = [] self.mms_root = mms_root self.version_header_deps = [] self.versionlib_deps = dict() self.all_targets = [] self.target_archs = set() self.metadata = { 'name': 'sample_mm', 'alias': 'sample', 'display_name': 'Sample Plugin', 'description': 'Sample basic plugin', 'author': 'AlliedModders LLC', 'url': 'http://www.sourcemm.net/', 'log_tag': 'SAMPLE', 'license': 'Public Domain', 'version': '1.0.0.{{git-shorthash}}', } if builder.options.targets: target_archs = builder.options.targets.split(',') else: target_archs = ['x86', 'x86_64'] for arch in target_archs: try: cxx = builder.DetectCxx(target_arch = arch) self.target_archs.add(cxx.target.arch) except Exception as e: # Error if archs were manually overridden. if builder.options.targets: raise print('Skipping target {}: {}'.format(arch, e)) continue self.all_targets.append(cxx) if not self.all_targets: raise Exception('No suitable C/C++ compiler was found.') def findSdkPath(self, sdk_name): dir_name = 'hl2sdk-{}'.format(sdk_name) hl2sdk_root = builder.options.hl2sdk_root if not hl2sdk_root: hl2sdk_root = resolveEnvPath('HL2SDKROOT') if hl2sdk_root: sdk_path = os.path.abspath(os.path.normpath(os.path.join(hl2sdk_root, dir_name))) if os.path.exists(sdk_path): return sdk_path return resolveEnvPath('HL2SDK{}'.format(sdk_name.upper()), dir_name) def detectSDKs(self): sdk_list = [s for s in builder.options.sdks.split(',') if s] SdkHelpers.find_sdk_path = self.findSdkPath SdkHelpers.findSdks(builder, self.all_targets, sdk_list) self.sdks = SdkHelpers.sdks self.sdk_manifests = SdkHelpers.sdk_manifests self.sdk_targets = SdkHelpers.sdk_targets for sdk_target in self.sdk_targets: if not sdk_target.sdk['source2']: raise Exception('Only Source2 games are supported by this script.') def addVersioning(self, cxx): cxx.includes += [ os.path.join(builder.buildPath, 'versioning') ] # Nasty hack to force use lib from ambuild in the vs solution # otherwise it'll be required to compile versionlib separately as a vs solution # which is very annoying to have versionlib = self.versionlib_deps[cxx.target.arch] if builder.options.generator == 'vs': target_path = TargetSuffix(cxx.target) cxx.postlink += [ os.path.join(builder.buildPath, 'versionlib', target_path, 'versionlib.lib') ] cxx.sourcedeps += [ versionlib ] else: cxx.postlink += [ versionlib ] cxx.sourcedeps += self.version_header_deps def generateVersioningHeaders(self): builder.SetBuildFolder('/') # Generate metadata plugin header, which would cause plugin recompilation if changed self.version_header_deps += [ builder.AddOutputFile(os.path.join('versioning', 'version_gen.h'), f""" #ifndef _PLUGIN_METADATA_INFORMATION_H_ #define _PLUGIN_METADATA_INFORMATION_H_ #define PLUGIN_NAME \"{self.metadata['name']}\" #define PLUGIN_ALIAS \"{self.metadata['alias']}\" #define PLUGIN_DISPLAY_NAME \"{self.metadata['display_name']}\" #define PLUGIN_DESCRIPTION \"{self.metadata['description']}\" #define PLUGIN_AUTHOR \"{self.metadata['author']}\" #define PLUGIN_URL \"{self.metadata['url']}\" #define PLUGIN_LOGTAG \"{self.metadata['log_tag']}\" #define PLUGIN_LICENSE \"{self.metadata['license']}\" extern const char* PLUGIN_FULL_VERSION; extern const char* PLUGIN_REVISION_SHORT; extern const char* PLUGIN_REVISION_LONG; extern const char* PLUGIN_REVISION_COUNT; #endif /* _PLUGIN_METADATA_INFORMATION_H_ */ """.encode('utf-8')) ] def generateVersionLib(self): builder.SetBuildFolder('/') version = self.metadata['version'] direct_head, git_head_path = getGitHeadPath() shorthash = longhash = revision_count = '0' if direct_head and git_head_path: # Force track head changes to trigger recompilation builder.AddConfigureFile(git_head_path) builder.AddConfigureFile(direct_head) revision_count = runAndReturn(['git', 'rev-list', '--count', 'HEAD']) revision_hash = runAndReturn(['git', 'log', '--pretty=format:%h:%H', '-n', '1']) shorthash, longhash = revision_hash.split(':') if '{{git-shorthash}}' in version: version = version.replace('{{git-shorthash}}', shorthash) if '{{git-longhash}}' in version: version = version.replace('{{git-longhash}}', longhash) if '{{git-count}}' in version: version = version.replace('{{git-count}}', revision_count) # For the version strings and git hashes compile that to a static library instead, # to prevent a full recompilation on version/git changes which would only trigger this cpp # and linking to be performed versionlib_cpp = builder.AddOutputFile(os.path.join('versioning', 'versionlib.cpp'), f""" const char *PLUGIN_FULL_VERSION = \"{version}\"; const char *PLUGIN_REVISION_SHORT = \"{shorthash}\"; const char *PLUGIN_REVISION_LONG = \"{longhash}\"; const char *PLUGIN_REVISION_COUNT = \"{revision_count}\"; """.encode('utf-8')) for cxx in self.all_targets: lib = cxx.StaticLibrary(f'versionlib_{cxx.target.arch}') self.configureCXX(lib.compiler) # VS generator doesn't like having non string sources, but we need that for correct # dependency tracking if builder.options.generator == 'vs': lib.sources += [ os.path.join(builder.buildPath, 'versioning', 'versionlib.cpp') ] lib.compiler.sourcedeps += [ versionlib_cpp ] else: lib.sources += [ versionlib_cpp ] self.versionlib_deps[cxx.target.arch] = builder.Add(lib).binary def configure(self): for cxx in self.all_targets: if cxx.target.arch not in ['x86', 'x86_64']: raise Exception('Unknown target architecture: {0}'.format(arch)) self.configureCXX(cxx) def configurePluginMetadata(self): plugin_metadata_path = os.path.join(builder.sourcePath, 'plugin-metadata.json') if not os.path.exists(plugin_metadata_path): with open(plugin_metadata_path, 'w') as f: json.dump(self.metadata, f, indent = 4) else: obj = None with open(plugin_metadata_path, 'r') as f: obj = json.load(f) self.metadata.update(obj) # If some fields are missing, add them back in from a sample metadata if len(obj) < len(self.metadata): with open(plugin_metadata_path, 'w') as f: json.dump(self.metadata, f, indent = 4) self.generateVersioningHeaders() self.generateVersionLib() def configureCXX(self, cxx): if cxx.behavior == 'gcc': cxx.defines += [ 'stricmp=strcasecmp', '_stricmp=strcasecmp', '_snprintf=snprintf', '_vsnprintf=vsnprintf', 'HAVE_STDINT_H', 'GNUC', ] cxx.cflags += [ '-pipe', '-fno-strict-aliasing', '-Wall', '-Werror', '-Wno-sign-compare', '-Wno-uninitialized', '-Wno-unused', '-Wno-switch', '-msse', '-fPIC', ] cxx.cxxflags += ['-std=c++17'] if (cxx.version >= 'gcc-4.0') or cxx.family == 'clang': cxx.cflags += ['-fvisibility=hidden'] cxx.cxxflags += ['-fvisibility-inlines-hidden'] cxx.cxxflags += [ '-fno-exceptions', '-fno-threadsafe-statics', '-Wno-non-virtual-dtor', '-Wno-overloaded-virtual', '-Wno-register', ] if (cxx.version >= 'gcc-4.7' or cxx.family == 'clang'): cxx.cxxflags += ['-Wno-delete-non-virtual-dtor'] if cxx.family == 'gcc': cxx.cflags += ['-mfpmath=sse'] if cxx.family == 'clang': cxx.cxxflags += ['-Wno-implicit-exception-spec-mismatch'] if cxx.version >= 'clang-3.9': cxx.cxxflags += ['-Wno-expansion-to-defined'] if cxx.version >= 'clang-3.6': cxx.cxxflags += ['-Wno-inconsistent-missing-override'] if cxx.version >= 'clang-3.4': cxx.cxxflags += ['-Wno-deprecated-register'] else: cxx.cxxflags += ['-Wno-deprecated'] # Work around SDK warnings. if cxx.version >= 'clang-10.0': cxx.cflags += [ '-Wno-implicit-int-float-conversion', '-Wno-tautological-overlap-compare', ] elif cxx.like('msvc'): if builder.options.debug == '1': cxx.cflags += ['/MTd'] cxx.linkflags += ['/NODEFAULTLIB:libcmt'] else: cxx.cflags += ['/MT'] cxx.defines += [ '_CRT_SECURE_NO_DEPRECATE', '_CRT_SECURE_NO_WARNINGS', '_CRT_NONSTDC_NO_DEPRECATE', ] cxx.cflags += [ '/W3', '/Zi', '/std:c++17', ] cxx.cxxflags += ['/TP'] cxx.linkflags += [ '/SUBSYSTEM:WINDOWS', 'kernel32.lib', 'user32.lib', 'gdi32.lib', 'winspool.lib', 'comdlg32.lib', 'advapi32.lib', 'shell32.lib', 'ole32.lib', 'oleaut32.lib', 'uuid.lib', 'odbc32.lib', 'odbccp32.lib', ] # Optimization if builder.options.opt == '1': cxx.defines += ['NDEBUG'] if cxx.behavior == 'gcc': cxx.cflags += ['-O3'] elif cxx.behavior == 'msvc': cxx.cflags += ['/Ox', '/Zo'] cxx.linkflags += ['/OPT:ICF', '/OPT:REF'] # Debugging if builder.options.debug == '1': cxx.defines += ['DEBUG', '_DEBUG'] if cxx.behavior == 'gcc': cxx.cflags += ['-g3'] elif cxx.behavior == 'msvc': cxx.cflags += ['/Od', '/RTC1'] # Don't omit the frame pointer. # This needs to be after our optimization flags which could otherwise disable it. if cxx.behavior == 'gcc': cxx.cflags += ['-fno-omit-frame-pointer'] elif cxx.behavior == 'msvc': cxx.cflags += ['/Oy-'] # Platform-specifics if cxx.target.platform == 'linux': cxx.defines += ['LINUX', '_LINUX', 'POSIX', '_FILE_OFFSET_BITS=64'] if cxx.family == 'gcc': cxx.linkflags += ['-static-libgcc'] elif cxx.family == 'clang': cxx.linkflags += ['-lgcc_eh'] cxx.linkflags += ['-static-libstdc++'] elif cxx.target.platform == 'windows': cxx.defines += ['WIN32', '_WINDOWS'] def Library(self, cxx, name): binary = cxx.Library(name) return binary def HL2Library(self, context, compiler, name, sdk): binary = self.Library(compiler, name) mms_core_path = os.path.join(self.mms_root, 'core') cxx = binary.compiler self.addVersioning(cxx) cxx.cxxincludes += [ os.path.join(context.currentSourcePath), os.path.join(mms_core_path), os.path.join(mms_core_path, 'sourcehook'), ] for other_sdk in self.sdk_manifests: cxx.defines += ['SE_{}={}'.format(other_sdk['define'], other_sdk['code'])] if sdk['source2']: cxx.defines += ['META_IS_SOURCE2'] binary.sources += [ os.path.join(sdk['path'], 'public', 'tier0', 'memoverride.cpp'), os.path.join(sdk['path'], 'tier1', 'convar.cpp'), ] SdkHelpers.configureCxx(context, binary, sdk) cxx.linkflags += additionalLibs(context, binary, sdk) cxx.defines += additionalDefines(context, binary, sdk) cxx.cxxincludes += additionalIncludes(context, binary, sdk) context.AddConfigureFile(os.path.join(context.currentSourcePath, 'plugin-metadata.json')) return binary MMSPlugin = MMSPluginConfig() MMSPlugin.detectSDKs() MMSPlugin.configurePluginMetadata() MMSPlugin.configure() BuildScripts = [ 'AMBuilder', 'PackageScript', ] builder.Build(BuildScripts, { 'MMSPlugin': MMSPlugin })