def run(self): # If the build starts with the project name, we know this is a game project being built is_game_project = self.build_name.startswith(self.config.uproject_name) print_action('{} {}'.format( 'Building' if not self.config.clean else 'Cleaning', self.build_name)) cmd_args = [ self.build_name, self.config.platform, self.config.configuration ] if is_game_project: cmd_args.append(self.config.uproject_file_path) cmd_args += ['-NoHotReload', '-waitmutex'] if get_visual_studio_version() == 2017: cmd_args.append('-2017') # Do any pre cleaning if self.config.clean or self.force_clean: if is_game_project: self.clean_game_project_folder() else: if launch(self.config.UE4CleanBatchPath, cmd_args) != 0: self.error = 'Failed to clean project {}'.format( self.build_name) return False # Do the actual build if launch(self.config.UE4BuildBatchPath, cmd_args) != 0: self.error = 'Failed to build "{}"!'.format(self.build_name) return False return True
def run(self): if not self.config.automated: # First make sure we actually have git credentials ssh_path = os.path.join(os.environ['USERPROFILE'], '.ssh') if not os.path.exists(ssh_path) and self.rsa_path != '': rsa_file = os.path.join(self.config.uproject_dir_path, self.rsa_path) if not os.path.isfile(rsa_file): self.error = 'No git credentials exists at rsa_path! ' \ 'Check rsa_path is relative to the project path and exists.' return False os.mkdir(ssh_path) shutil.copy2(rsa_file, ssh_path) # To get around the annoying user prompt, lets just set github to be trusted, no key checking if self.disable_strict_hostkey_check: if not os.path.isfile(os.path.join(ssh_path, 'config')): with open(os.path.join(ssh_path, 'config'), 'w') as fp: fp.write('Host github.com\nStrictHostKeyChecking no') output_dir = os.path.join(self.config.uproject_dir_path, self.output_folder) if self.force_repull and os.path.exists(output_dir): print_action("Deleting Unreal Engine Folder for a complete re-pull") def on_rm_error(func, path, exc_info): # path contains the path of the file that couldn't be removed # let's just assume that it's read-only and unlink it. del func # unused if exc_info[0] is not FileNotFoundError: os.chmod(path, stat.S_IWRITE) os.unlink(path) # Forcing a re-pull, delete the whole engine directory! shutil.rmtree(output_dir, onerror=on_rm_error) if not os.path.isdir(output_dir): os.makedirs(output_dir) if not os.path.isdir(os.path.join(output_dir, '.git')): print_action("Cloning from Git '{}' branch '{}'".format(self.repo_name, self.branch_name)) cmd_args = ['clone', '-b', self.branch_name, self.repo_name, output_dir] err = launch('git', cmd_args) if err != 0: self.error = 'Git clone failed!' return False else: with push_directory(output_dir): print_action("Pulling from Git '{}' branch '{}'".format(self.repo_name, self.branch_name)) err = launch('git', ['pull', 'origin', self.branch_name], silent=True) if err != 0: self.error = 'Git pull failed!' return False return True
def client(config: ProjectConfig, extra, ip): """ Run the client """ print_action('Running Client') cmd_args = ['-game', '-windowed', '-ResX=1280', '-ResY=720'] cmd_args.extend(['-' + arg.strip() for arg in extra.split('-')[1:]]) if ip != '': cmd_args.insert(0, ip) client_exe_path = os.path.join( config.uproject_dir_path, 'builds\\WindowsNoEditor\\{0}\\Binaries\\' 'Win64\\{0}.exe'.format(config.uproject_name)) if not os.path.isfile(client_exe_path): error_exit('Client is not built!', not config.automated) launch(client_exe_path, cmd_args, True, should_wait=False)
def server(config: ProjectConfig, extra, umap): """ Run a server """ print_action('Running Server') cmd_args = [] cmd_args.extend(['-' + arg.strip() for arg in extra.split('-')[1:]]) if umap != '': cmd_args.insert(0, umap) server_exe_path = os.path.join( config.uproject_dir_path, 'builds\\WindowsServer\\{0}\\Binaries\\' 'Win64\\{0}Server.exe'.format(config.uproject_name)) if not os.path.isfile(server_exe_path): error_exit('Server is not built!', not config.automated) launch(server_exe_path, cmd_args, True, should_wait=False)
def run(self): exe_path = 'UE4Editor-Win64-Debug-Cmd.exe' if self.config.debug else 'UE4Editor-Cmd.exe' exe_path = os.path.join(self.config.UE4EnginePath, 'Engine/Binaries/Win64', exe_path) if not os.path.isfile(exe_path): self.error = 'Unable to resolve path to unreal cmd "{}"'.format(exe_path) return False # Cook command parameters cmd_args = ['-run=Cook'] for map_name in self.maps: cmd_args.append('-Map={}'.format(map_name)) for dir_name in self.cook_dirs: cmd_args.append('-CookDir={}'.format(dir_name)) if len(self.cultures) > 0: cmd_args.append('-CookCultures={}'.format('+'.join(self.cultures))) cmd_args.append('-NoLogTimes') # General Parameters cmd_args.extend(['-TargetPlatform={}'.format(self.config.platform), '-Unversioned']) cmd_args.append('-output_dir={}'.format(self.output_dir)) if self.config.debug: cmd_args.append('-debug') if launch(exe_path, cmd_args) != 0: self.error = 'Unable to complete cook action. Check output.' return False return True
def standalone_func(config: ProjectConfig, extra, ip, waittime, umap): """ Run a standalone build of the game """ print_action('Running Standalone') cmd_args = [ config.uproject_file_path, '-game', '-windowed', '-ResX=1280', '-ResY=720' ] cmd_args.extend(['-' + arg.strip() for arg in extra.split('-')[1:]]) if ip != '': time.sleep(waittime) cmd_args.insert(1, ip) if umap != '': cmd_args.insert(1, umap) launch(config.UE4EditorPath, cmd_args, True, should_wait=False)
def genloc(config): """ Generate localization """ print_action('Generating Localization') cmd_args = [ config.uproject_file_path, '-Run=GatherText', '-config={}'.format(config.proj_localization_script), '-log' ] if launch(config.UE4EditorPath, cmd_args) != 0: error_exit('Failed to generate localization, see errors...') click.pause()
def genproj(config): """ Generate project file """ print_action('Generating Project Files') cmd_args = [ '-ProjectFiles', '-project={}'.format(config.uproject_file_path), '-game', '-engine' ] if get_visual_studio_version() == 2017: cmd_args.append('-2017') if launch(config.UE4UBTPath, cmd_args) != 0: error_exit('Failed to generate project files, see errors...')
def genproj_func(config: ProjectConfig, run_it): """ Generate project file """ print_action('Generating Project Files') cmd_args = [ '-ProjectFiles', '-project={}'.format(config.uproject_file_path), '-game', '-engine' ] if config.engine_minor_version <= 25: cmd_args.append('-VS{}'.format( get_visual_studio_version(config.get_suitable_vs_versions()))) if launch(config.UE4UBTPath, cmd_args) != 0: error_exit('Failed to generate project files, see errors...', not config.automated) if run_it: launch(os.path.join(config.uproject_dir_path, config.uproject_name + '.sln'), separate_terminal=True, should_wait=False)
def compile_all_blueprints(config: ProjectConfig): print_action('Compiling All Blueprints') cmd_args = [ config.uproject_file_path, '-run=CompileAllBlueprints', '-autocheckout', '-projectonly', '-unattended' ] if launch(config.UE4EditorPath, cmd_args) != 0: error_exit('Failed to compile all blueprints, see errors...', not config.automated) if not config.automated: click.pause()
def fix_redirects(config: ProjectConfig): print_action('Fixing Redirectors') cmd_args = [ config.uproject_file_path, '-run=ResavePackages', '-fixupredirects', '-autocheckout', '-projectonly', '-unattended' ] if launch(config.UE4EditorPath, cmd_args) != 0: error_exit('Failed to fixup redirectors, see errors...', not config.automated) if not config.automated: click.pause()
def run(self): if self.config.clean: return True if self.config.clean: return True template_file_path = os.path.join(self.config.uproject_dir_path, self.steam_app_template) auto_file_path = os.path.join( self.config.uproject_dir_path, self.steam_app_dir, '{}_build.vdf'.format(self.config.uproject_name.lower())) self.create_app_build_script( template_file_path, auto_file_path, '..\\..\\..\\..\\..\\builds\\{}'.format(self.build_name), '{} Version {}'.format(self.config.uproject_name, self.config.version_str), self.set_live) try: steam_app_id_name = 'steam_appid.txt' shutil.copy2( os.path.join(self.config.uproject_dir_path, steam_app_id_name), os.path.join(self.config.builds_path, '{}\\steam_appid.txt'.format(self.build_name))) if self.install_script_rel_path != '': shutil.copy2( os.path.join(self.config.uproject_dir_path, 'steam_redist_installscript.vdf'), os.path.join(self.config.builds_path, self.install_script_rel_path)) print_action('Steam required files inserted into build') except Exception: pass print_action('Uploading {} Build to Steam'.format( self.config.uproject_name)) cmd_args = [ '+login', os.environ[STEAMWORKS_USER_ENV_VAR], os.environ[STEAMWORKS_PASS_ENV_VAR], '+run_app_build', '..\\scripts\\{}_build.vdf'.format( self.config.uproject_name.lower()), '+quit' ] if launch( os.path.join(self.config.uproject_dir_path, self.builder_exe_path), cmd_args) != 0: self.error = 'Unable to upload build {} to steam!'.format( self.config.uproject_name) return False return True
def do_project_build(extra_args=None): args = [ os.path.join(os.path.dirname(__file__), 'build_script.py'), '-s', '{}'.format(script_file_path), '-t', 'Editor' ] if extra_args is not None: args.extend(extra_args) result = launch(os.path.join( os.environ.get("PYTHON_HOME", ".").replace('"', ''), "python.exe"), args, False, should_wait=True) return result == 0
def do_build(self, build_name): # If the build starts with the project name, we know this is a game project being built is_game_project = build_name.startswith(self.config.uproject_name) print_action('{} {}'.format( 'Building' if not self.config.clean else 'Cleaning', build_name)) cmd_args = [ build_name, self.config.platform, self.config.configuration ] if is_game_project: cmd_args.append(self.config.uproject_file_path) cmd_args += ['-NoHotReload', '-waitmutex'] if self.config.engine_minor_version <= 25: cmd_args.append('-VS{}'.format( get_visual_studio_version( self.config.get_suitable_vs_versions()))) else: # Engine versions greater than 25 can determine visual studios location and will do it automatically. # We include -FromMsBuild which is common beyond version 25 but it is just a format specifier. cmd_args.append('-FromMsBuild') # Do any pre cleaning if self.config.clean or self.force_clean: if is_game_project: self.clean_game_project_folder() else: if launch(self.config.UE4CleanBatchPath, cmd_args) != 0: self.error = 'Failed to clean project {}'.format( build_name) return False # Do the actual build if launch(self.config.UE4BuildBatchPath, cmd_args) != 0: self.error = 'Failed to build "{}"!'.format(build_name) return False return True
def run(self): pak_list_file_path = os.path.join( os.getcwd(), '{}_pak_list.txt'.format(self.pak_name)) with open(pak_list_file_path, 'w') as fp: for content_path in self.content_paths: asset_paths = glob.glob( os.path.join(self.content_dir, content_path) + os.path.sep + '**', recursive=True) for asset_path in asset_paths: if os.path.isdir(asset_path): continue reduced_root = asset_path.replace(self.content_dir, '') if reduced_root[0] == '\\' or reduced_root[0] == '/': reduced_root = reduced_root[1:] content_asset_path = os.path.join(self.asset_root_path, reduced_root) write_line = '"{0}" "{1}" -compress\n'.format( asset_path, content_asset_path.replace('\\', '/')) fp.write(write_line) unreal_pak_path = os.path.join( self.config.UE4EnginePath, 'Engine\\Binaries\\Win64\\UnrealPak.exe') if not os.path.isfile(unreal_pak_path): self.error = 'Unable to find path to UnrealPak.exe. Is it compiled?' return False cmd_args = [ os.path.join(self.config.uproject_dir_path, self.output_dir, self.pak_name + '.pak'), '-create={}'.format(pak_list_file_path), '-encryptionini', '-enginedir={}'.format(self.config.UE4EnginePath), '-projectdir={}'.format(self.config.uproject_dir_path), '-platform={}'.format(self.config.platform), '-UTF8Output', '-multiprocess' ] if launch(unreal_pak_path, cmd_args) != 0: self.error = 'Unable to pak!' return False return True
def run(self): if self.config.editor_running: self.warning('You are packaging while also running the editor. ' 'This could fail because of memory contraints.') # click.secho('Building for client version {}'.format(self.config.version_str)) def on_rm_error(func, path, exc_info): # path contains the path of the file that couldn't be removed # let's just assume that it's read-only and unlink it. del func # Unused if exc_info[0] is not FileNotFoundError: os.chmod(path, stat.S_IWRITE) os.unlink(path) if self.config.clean: # Kill the build directories if self.build_type == 'client': shutil.rmtree(os.path.join( self.config.builds_path, '{}Client'.format( platform_long_names[self.config.platform])), onerror=on_rm_error) elif self.build_type == 'server': shutil.rmtree(os.path.join( self.config.builds_path, '{}Server'.format( platform_long_names[self.config.platform])), onerror=on_rm_error) else: shutil.rmtree(os.path.join( self.config.builds_path, '{}{}'.format( platform_long_names[self.config.platform], 'NoEditor' if self.no_compile_editor else '')), onerror=on_rm_error) # cap_build_name = self.config.uproject_name.title() # print_action('Building {} Build'.format(cap_build_name)) build_blacklist_file_path = os.path.join( self.config.uproject_dir_path, self.build_blacklist_dir.format(self.config.platform), self.blacklist_file_name.format(self.config.configuration)) if self.content_black_list != '': print_action( 'Setting up content blacklist for configuration {}'.format( self.config.configuration)) if os.path.isfile(build_blacklist_file_path): os.unlink(build_blacklist_file_path) os.makedirs(os.path.join( self.config.uproject_dir_path, self.build_blacklist_dir.format(self.config.platform)), exist_ok=True) shutil.copyfile( os.path.join(self.config.uproject_dir_path, self.content_black_list), build_blacklist_file_path) cmd_args = [ '-ScriptsForProject={}'.format(self.config.uproject_file_path), 'BuildCookRun', '-NoHotReload', '-nop4', '-project={}'.format(self.config.uproject_file_path), '-archivedirectory={}'.format(self.config.builds_path), '-clientconfig={}'.format(self.config.configuration), '-serverconfig={}'.format(self.config.configuration), '-ue4exe={}'.format('UE4Editor-Win64-Debug-Cmd.exe' if self.config. debug else 'UE4Editor-Cmd.exe'), '-prereqs', '-targetplatform={}'.format(self.config.platform), '-platform={}'.format(self.config.platform), '-servertargetplatform={}'.format(self.config.platform), '-serverplatform={}'.format(self.config.platform), '-CrashReporter', '-utf8output' ] if self.no_compile_editor: cmd_args.append('-nocompileeditor') if self.build: cmd_args.append('-build') if self.cook: cmd_args.append('-cook') if self.package: cmd_args.append('-package') if self.stage: cmd_args.append('-stage') if self.archive: cmd_args.append('-archive') if self.build_type == 'client': cmd_args.append('-client') elif self.build_type == 'server': cmd_args.extend(['-server', '-noclient']) if self.nativize_assets: cmd_args.append('-nativizeAssets') if self.pak_assets and self.stage: cmd_args.append('-pak') if self.compressed_assets: cmd_args.append('-compressed') if self.no_debug_info: cmd_args.append('-nodebuginfo') if self.no_editor_content: cmd_args.append('-SkipCookingEditorContent') if self.ignore_cook_errors: cmd_args.append('-IgnoreCookErrors') if len(self.cook_output_dir) > 0: cmd_args.append('-CookOutputDir={}'.format( os.path.join(self.config.uproject_dir_path, self.cook_output_dir))) if len(self.maps) > 0: cmd_args.append('-map={}'.format('+'.join(self.maps))) if len(self.cook_dirs) > 0: cmd_args.append('-cookdir={}'.format('+'.join(self.cook_dirs))) # TODO: determine engine bug or issue in this script. Fails if previous cooked content exists already. if len(self.cook_output_dir) > 0: # manual clean everytime because of bug... shutil.rmtree(os.path.join(self.config.uproject_dir_path, self.cook_output_dir), onerror=on_rm_error) cmd_args.extend(['-iterate', '-iterativecooking']) else: if self.config.clean or self.full_rebuild: cmd_args.append('-clean') else: cmd_args.extend(['-iterate', '-iterativecooking']) cmd_args.append('-compile') # print_action('Building, Cooking, and Packaging {} Build'.format(cap_build_name)) if launch(self.config.UE4RunUATBatPath, cmd_args) != 0: self.error = 'Unable to build {}!'.format( self.config.uproject_name) return False # Don't leave blacklist around if os.path.isfile(build_blacklist_file_path): os.unlink(build_blacklist_file_path) return True
def runeditor_func(config: ProjectConfig): """ Run the editor with the registered project """ print_action('Running Editor') launch(config.UE4EditorPath, [config.uproject_file_path], True, should_wait=False)
def run(self): if not self.config.automated: # First make sure we actually have git credentials ssh_path = os.path.join(os.environ['USERPROFILE'], '.ssh') if not os.path.exists(ssh_path) and self.rsa_path != '': rsa_file = os.path.join(self.config.uproject_dir_path, self.rsa_path) if not os.path.isfile(rsa_file): self.error = 'No git credentials exists at rsa_path! ' \ 'Check rsa_path is relative to the project path and exists.' return False os.mkdir(ssh_path) shutil.copy2(rsa_file, ssh_path) # To get around the annoying user prompt, lets just set github to be trusted, no key checking if self.disable_strict_hostkey_check: if not os.path.isfile(os.path.join(ssh_path, 'config')): with open(os.path.join(ssh_path, 'config'), 'w') as fp: fp.write('Host github.com\nStrictHostKeyChecking no') output_dir = os.path.join(self.config.uproject_dir_path, self.output_folder) if not os.path.isdir(output_dir): os.makedirs(output_dir) elif os.path.isdir(os.path.join(output_dir, '.git')): # check if the repo in the folder is on the correct branch. If not, delete the folder so we can # start over. with push_directory(output_dir, False): cur_branch = self.get_current_branch() if cur_branch != self.branch_name: if cur_branch in self.similar_branches and self.branch_name in self.similar_branches: do_branch_switch = click.confirm( 'Branch mismatch but both branches are similar. ' 'Do branch switch? (If you have unsaved changes in this repo ' 'this will clobber them!)') if not do_branch_switch: self.error = 'Clean up your repo manually so a branch switch can be made.' return False err = launch('git', ['fetch', 'origin']) if err != 0: self.error = 'Git fetch failed...' return False # Cleanup before branch switch launch('git', ['checkout', '--', '*']) cmd_args = [ 'checkout', '-b', self.branch_name, 'origin/{}'.format(self.branch_name) ] err = launch('git', cmd_args) if err != 0: ask_do_repull = click.confirm( 'Tried to switch branches but failed. ' 'Would you like to clobber and re-pull?') if ask_do_repull: self.force_repull = True else: self.error = 'Please correct the issue manually. Check the errors above for hints.' return False else: # Cleanup again just in case launch('git', ['checkout', '--', '*']) self.branch_switched = True else: ask_do_repull = click.confirm( 'Branch mismatch ("{}" should equal "{}"). Clobber this entire ' 'repo and do a re-pull?'.format( cur_branch, self.branch_name), default=False) if ask_do_repull: self.force_repull = True else: self.error = 'Branch mismatch caused pull to be halted. Please correct the issue manually.' return False if self.force_repull: print_action( "Deleting the folder '{}' for a complete re-pull".format( self.output_folder)) def on_rm_error(func, path, exc_info): # path contains the path of the file that couldn't be removed # let's just assume that it's read-only and unlink it. del func # unused if exc_info[0] is not FileNotFoundError: os.chmod(path, stat.S_IWRITE) os.unlink(path) # Forcing a re-pull, delete the whole directory! try: shutil.rmtree(output_dir, onerror=on_rm_error) except Exception: self.error = 'Unable to delete the repo directory for a re-pull. ' \ 'Check that the files are not open / held by another process and try again.' return False os.makedirs(output_dir) if not os.path.isdir(os.path.join(output_dir, '.git')): print_action("Cloning from Git '{}' branch '{}'".format( self.repo_name, self.branch_name)) cmd_args = [ 'clone', '-b', self.branch_name, self.repo_name, output_dir ] err = launch('git', cmd_args) if err != 0: self.error = 'Git clone failed!' return False else: with push_directory(output_dir, False): print_action("Pulling from Git '{}' branch '{}'".format( self.repo_name, self.branch_name)) err = launch('git', ['pull', 'origin', self.branch_name], silent=True) if err != 0: self.error = 'Git pull failed!' return False return True
def ensure_engine(config, engine_override): """ Pre-work step of ensuring we have a valid engine and enough components exist to do work :param config: The project configuration (may not point to a valid engine yet) :param engine_override: The desired engine directory path to use """ can_pull_engine = config.git_repo != '' and config.git_proj_branch != '' if config.UE4EnginePath == '': if not can_pull_engine and engine_override == '': error_exit( 'Static engine placement required for non-git pulled engine. ' 'You can specify a path using the -e param, or specify git configuration.', not config.automated) if engine_override != '': config.setup_engine_paths(os.path.abspath(engine_override)) elif not config.automated: result = click.confirm( 'Would you like to specify the location of the engine install?', default=False) if result: result = click.prompt( 'Where would you like to install the engine?') if not os.path.exists(result): try: os.makedirs(result) except Exception: error_exit( 'Unable to create engine directory! Tried @ {}'. format(result), not config.automated) config.setup_engine_paths(result) else: # Find an ideal location to put the engine if len(config.engine_path_name) == 0: # Put the engine one directory down from the uproject engine_path = os.path.abspath( os.path.join( config.uproject_dir_path, '..\\UnrealEngine_{}'.format( config.uproject_name))) else: if os.path.isabs(config.engine_path_name): engine_path = config.engine_path_name else: engine_path = os.path.normpath( os.path.join(config.uproject_dir_path, config.engine_path_name)) if not os.path.exists(engine_path): try: os.makedirs(engine_path) except Exception: error_exit( 'Unable to create engine directory! Tried @ {}'. format(engine_path), not config.automated) config.setup_engine_paths(engine_path) else: error_exit( 'No engine available for automated case! Either fill out git info or supply engine directory', not config.automated) elif config.UE4EnginePath != engine_override and engine_override != '': error_exit( 'Specific engine path requested, but engine path for this project already exists?', not config.automated) # Before doing anything, make sure we have all build dependencies ready if can_pull_engine: git_action = Git(config) git_action.branch_name = config.git_proj_branch git_action.repo_name = config.git_repo git_action.output_folder = config.UE4EnginePath git_action.disable_strict_hostkey_check = True git_action.force_repull = False if not git_action.run(): error_exit(git_action.error, not config.automated) if not config.setup_engine_paths(engine_override): error_exit('Could not setup valid engine paths!', not config.automated) # Register the engine (might do nothing if already registered) # If no key name, this is an un-keyed static engine. if config.UE4EngineKeyName != '' and not config.automated: register_project_engine(config, False) if not config.editor_running: print_action('Checking engine dependencies up-to-date') def add_dep_exclude(path_name, args): args.append('-exclude={}'.format(path_name)) cmd_args = [] if config.exclude_samples: for sample_pack in ['FeaturePacks', 'Samples']: add_dep_exclude(sample_pack, cmd_args) for extra_exclude in config.extra_dependency_excludes: add_dep_exclude(extra_exclude, cmd_args) if launch(config.UE4GitDependenciesPath, cmd_args) != 0: error_exit('Engine dependencies Failed to Sync!', not config.automated) if not os.path.isfile(config.UE4UBTPath): # The unreal build tool does not exist, we need to build it first # We use the generate project files batch script because it ensures the build tool exists, # and builds it if not. print_action( "Build tool doesn't exist yet, generating project and building..." ) if launch(config.UE4GenProjFilesPath, ['-2017'] if get_visual_studio_version() == 2017 else []) != 0: error_exit('Failed to build UnrealBuildTool.exe!', not config.automated)