diff --git a/.gitignore b/.gitignore index a157fa6..aadfbbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build +/install /.idea /src/.idea /src/Pipfile.lock diff --git a/Jenkinsfile b/Jenkinsfile index c724079..842c263 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,6 +11,7 @@ import com.esri.zrh.jenkins.ce.CityEnginePipelineLibrary import com.esri.zrh.jenkins.ce.PrtAppPipelineLibrary import com.esri.zrh.jenkins.PslFactory import com.esri.zrh.jenkins.psl.UploadTrackingPsl +import com.esri.zrh.jenkins.ToolInfo @Field def psl = PslFactory.create(this, UploadTrackingPsl.ID) @Field def cepl = new CityEnginePipelineLibrary(this, psl) @@ -46,6 +47,8 @@ import com.esri.zrh.jenkins.psl.UploadTrackingPsl [ os: cepl.CFG_OS_WIN10, bc: cepl.CFG_BC_REL, tc: cepl.CFG_TC_VC1427, cc: cepl.CFG_CC_OPT, arch: cepl.CFG_ARCH_X86_64, houdini: '19.5' ], ] +@Field final List INSTALLER_CONFIG = [ [ os: cepl.CFG_OS_WIN10, bc: cepl.CFG_BC_REL, tc: cepl.CFG_TC_VC1427, cc: cepl.CFG_CC_OPT, arch: cepl.CFG_ARCH_X86_64 ] ] +@Field final List INSTALLER_HOUDINI_VERS = [ '18.5', '19.0', '19.5' ] // -- SETUP @@ -68,6 +71,10 @@ stage('build') { cepl.runParallel(taskGenBuild()) } +stage('installer') { + cepl.runParallel(taskGenInstaller()) +} + papl.finalizeRun('palladio', env.BRANCH_NAME) @@ -93,6 +100,12 @@ Map taskGenBuild() { return tasks; } +Map taskGenInstaller() { + Map tasks = [:] + tasks << cepl.generateTasks('pld-msi', this.&taskCreateInstaller, INSTALLER_CONFIG) + return tasks; +} + // -- TASK BUILDERS @@ -106,7 +119,7 @@ def taskRunTest(cfg) { cepl.cleanCurrentDir() unstash(name: SOURCE_STASH) dir(path: 'build') { - papl.runCMakeBuild(SOURCE, 'build_and_run_tests', cfg, []) + papl.runCMakeBuild(SOURCE, 'build_and_run_tests', cfg, [], JenkinsTools.CMAKE319) } junit('build/test/palladio_test_report.xml') } @@ -121,7 +134,18 @@ def taskBuildPalladio(cfg) { cepl.cleanCurrentDir() unstash(name: SOURCE_STASH) dir(path: 'build') { - papl.runCMakeBuild(SOURCE, BUILD_TARGET, cfg, defs) + papl.runCMakeBuild(SOURCE, BUILD_TARGET, cfg, defs, JenkinsTools.CMAKE319) + } + final String artifactPattern = "palladio-*" + + if(cfg.os == cepl.CFG_OS_WIN10){ + dir(path: 'build'){ + stashFile = cepl.findOneFile(artifactPattern) + echo("stashFile: '${stashFile}'") + stash(includes: artifactPattern, name: "houdini${cfg.houdini.replace('.', '_')}") + stashPath = "${stashFile.path}" + echo("file path to stash: '${stashPath}'") + } } def versionExtractor = { p -> @@ -132,5 +156,51 @@ def taskBuildPalladio(cfg) { def cls = (p =~ /.*palladio-.*\.(hdn.*)-(windows|linux)\..*/) return cls[0][1] + '.' + cepl.getArchiveClassifier(cfg) } - papl.publish('palladio', env.BRANCH_NAME, "palladio-*", versionExtractor, cfg, classifierExtractor) + papl.publish('palladio', env.BRANCH_NAME, artifactPattern, versionExtractor, cfg, classifierExtractor) } + +def taskCreateInstaller(cfg) { + final String appName = 'palladio-installer' + cepl.cleanCurrentDir() + unstash(name: SOURCE_STASH) + + String pyArgs = "" + + // fetch outputs from builds + dir(path: 'build'){ + dir(path: 'tmp'){ + INSTALLER_HOUDINI_VERS.each { mv -> + unstash(name: "houdini${mv.replace('.', '_')}") + def zipFile = cepl.findOneFile("*hdn${mv.replace('.', '-')}*.zip") + final String zipFileName = zipFile.name + unzip(zipFile:zipFileName) + pyArgs += "-h${mv.replace('.', '')} \"build\\tmp\\${zipFileName.take(zipFileName.lastIndexOf('.'))}\" " + } + } + } + pyArgs += "-bv ${env.BUILD_NUMBER} " + pyArgs += "-bd \"build\\build_msi\" " + + // Toolchain definition for building MSI installers. + final JenkinsTools compiler = cepl.getToolchainTool(cfg) + final def toolchain = [ + new ToolInfo(JenkinsTools.WIX, cfg), + new ToolInfo(compiler, cfg) + ] + + // Setup environment according to above toolchain definition. + withTool(toolchain) { + psl.runCmd('python "palladio.git\\deploy\\build.py" ' + pyArgs) + + def buildProps = papl.jpe.readProperties(file: 'build/build_msi/deployment.properties') + + final String artifactPattern = "build_msi/${buildProps.package_file}" + final def artifactVersion = { p -> buildProps.package_version } + + def classifierExtractor = { p -> + return cepl.getArchiveClassifier(cfg) + } + + papl.publish(appName, env.BRANCH_NAME, artifactPattern, artifactVersion, cfg, classifierExtractor) + } +} \ No newline at end of file diff --git a/README.md b/README.md index 3a91a7b..8d08ce7 100644 --- a/README.md +++ b/README.md @@ -243,10 +243,12 @@ It can be useful to put RPKs into an `rpk` sub-directory of your current Houdini * RedHat Enterprise Linux 7 or 8 and compatible (CentOS, Alma Linux, Rocky Linux, ...) ### Required Toolchain & Compiler -* [cmake 3.13 or later](https://cmake.org/download) +* [cmake 3.19 or later](https://cmake.org/download) * [conan 1.20 or later](https://www.conan.io/downloads) * Linux: GCC 9.3 * Windows: Visual Studio 2019 (MSVC 14.27) +* [WiX Toolset 3.11.1 or later](https://wixtoolset.org/docs/wix3/): Optional, required for building .msi installers +* Python 3.6 or later: Optional, required for building .msi installers ### Required Build Dependencies (Latest Release) * Installation of Houdini 18.5, 19.0 or 19.5 (see https://sidefx.com/download) @@ -314,6 +316,16 @@ See [Quick Start](#quick-start) how to launch Houdini with Palladio. 1. `nmake palladio_test` 1. Run `bin\palladio_test` +### Building Windows MSI installer +1. Open a MSVC 14.27 x64 shell (Visual Studio 2019) and `cd` to the Palladio git repository +1. for each houdini version build the binary files to a local install folder: + 1. `mkdir build/installer_XXY` (i.e. `installer_195`) + 1. `cd build/installer_XXY` + 1. `cmake -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../../install/houdiniXX.Y" ../../src` + 1. `nmake install` (the plugin will be installed to `install/houdiniXX.Y`) +1. From a terminal run `deploy\build.py` and provide the binary folders via `-hXXY "install/houdiniXX.Y"` +1. The MSI installer should now be located in `build/build_msi/` + ## Release Notes ### v2.0.0 Beta 1 (Okt 13, 2022) @@ -328,6 +340,7 @@ Required CityEngine version: 2022.0 or older. - CGA annotations will create different UI widgets, for example a color picker or file browser - Seed parameter to change the seed value in the `pldAssign` node - Icon to Palladio nodes +- MSI windows installer #### Changed: - Updated Procedural Runtime (PRT) to 2.6.8300 (corresponds to CityEngine 2022.0) @@ -337,6 +350,7 @@ Required CityEngine version: 2022.0 or older. #### Development: - Updated compilers (now using C++ 17) +- Updated required cmake version to 3.19 - Linux version is now compiled with GCC 9.3 by default - Updated test framework and build system - Cleaned up CMake files diff --git a/deploy/build.py b/deploy/build.py new file mode 100644 index 0000000..8126ff3 --- /dev/null +++ b/deploy/build.py @@ -0,0 +1,241 @@ +import argparse +import glob +import subprocess +import json +import shutil +import os +from pathlib import Path +from string import Template + +# Dirs and Paths +SCRIPT_DIR = Path(__file__).resolve().parent +CFG_PATH = SCRIPT_DIR / 'installer_config.json' +RESOURCE_DIR = SCRIPT_DIR / 'resources' + +VERSION_JSON_PATH = SCRIPT_DIR / '../version.json' +PACKAGE_JSON_PATH = SCRIPT_DIR / 'palladio.json' + +DEFAULT_BUILD_DIR = SCRIPT_DIR / '../build/build_msi' + +TEMPLATE_DIR = SCRIPT_DIR / 'templates' +DEPLOYMENT_PROPERTIES_TEMPLATE_PATH = TEMPLATE_DIR / 'deployment.properties.in' + +WIX_TEMPLATE_PATH = TEMPLATE_DIR / 'palladio.wxs.in' +WIX_DIRECTORY_TEMPLATE_PATH = TEMPLATE_DIR / 'palladio_directory.in' +WIX_FEATURE_TEMPLATE_PATH = TEMPLATE_DIR / 'palladio_feature.in' + +PROJECT_NAME = 'Palladio' +PLATFORM = 'win64' +PACKAGE_VENDOR_ESCAPED = 'Esri R&amp;D Center Zurich' + + +def get_palladio_version_string(includePre=False, build_version=''): + version_map = {} + delimiter = '.' + with open(VERSION_JSON_PATH, 'r') as version_file: + version_map = json.load(version_file) + version_string = version_map['PLD_VERSION_MAJOR'] + delimiter + \ + version_map['PLD_VERSION_MINOR'] + \ + delimiter + version_map['PLD_VERSION_PATCH'] + if includePre: + version_string += version_map['PLD_VERSION_PRE'] + if len(build_version) > 0: + version_string += '+b' + build_version + return version_string + + +def gen_installer_filename(build_version): + return PROJECT_NAME + '-installer-' + get_palladio_version_string(True, build_version) + '.msi' + + +def rel_to_os_dir(path): + return Path(path).absolute() + + +def clear_folder(path): + if path.exists(): + shutil.rmtree(path) + os.makedirs(path) + + +def run_wix_command(cmd, build_dir): + process = subprocess.run(args=[str(i) for i in cmd], cwd=build_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + print(process.args) + print(process.stdout) + print(process.stderr) + + +def wix_harvest(houdini_version, binaries_install_dir, build_dir): + output_path = build_dir / f'houdini{houdini_version}.wxs' + wxs_file = output_path + wxs_var = f'-dPalladioInstallComponentDir{houdini_version}={binaries_install_dir}' + heat_cmd = ['heat', 'dir', binaries_install_dir, '-dr', f'PalladioInstallComponentDir{houdini_version}', '-ag', + '-cg', f'PalladioInstallComponent{houdini_version}', '-sfrag', '-template fragment', '-var', f'wix.PalladioInstallComponentDir{houdini_version}', '-srd', '-sreg', '-o', output_path] + + run_wix_command(heat_cmd, build_dir) + + return wxs_file, wxs_var + + +def wix_compile(sources, vars, build_dir): + candle_cmd = ['candle', '-arch', 'x64', + build_dir / 'palladio.wxs'] + sources + vars + run_wix_command(candle_cmd, build_dir) + + objects = glob.glob(str(build_dir / '*.wixobj')) + return objects + + +def wix_link(objects, vars, build_dir, installer_filename): + light_cmd = ['light', f'-dWixUILicenseRtf={SCRIPT_DIR}\\resources\\license.rtf', '-dInstallDir=', '-ext', 'WixUIExtension', + '-cultures:en-us', '-ext', 'WixUtilExtension', '-o', f'{build_dir}\\' + installer_filename] + light_cmd.extend(objects) + light_cmd.extend(vars) + run_wix_command(light_cmd, build_dir) + + +def create_installer(houdini_versions, binary_folders, build_dir, installer_filename): + wxs_files = [] + wxs_vars = [] + for houdini_version_key in binary_folders: + rel_path_string = binary_folders[houdini_version_key] + major = houdini_versions[houdini_version_key]['major'] + minor = houdini_versions[houdini_version_key]['minor'] + + binary_path = rel_to_os_dir(rel_path_string) + + print(binary_path, " ", houdini_version_key) + wxs_file, wxs_var = wix_harvest( + str(major) + str(minor), binary_path, build_dir) + wxs_files.append(wxs_file) + wxs_vars.append(wxs_var) + + wix_objects = wix_compile(wxs_files, wxs_vars, build_dir) + wix_link(wix_objects, wxs_vars, build_dir, installer_filename) + + +def copy_binaries(binary_folders, build_dir): + clear_folder(build_dir) + + valid_binaries = {} + for binary_folder in binary_folders: + src_path = binary_folders[binary_folder] + if len(src_path) == 0: + continue + os_src_path = rel_to_os_dir(src_path) + if os_src_path.exists(): + binary_path = build_dir / 'install' / Path(binary_folder) + print("copy files from ", os_src_path, " to ", binary_path) + shutil.copytree(os_src_path / 'packages' / 'palladio', binary_path) + shutil.copytree(os_src_path / 'dso', binary_path / 'dso') + shutil.copytree(os_src_path / 'config', binary_path / 'config') + shutil.copy(PACKAGE_JSON_PATH, build_dir) + valid_binaries[binary_folder] = binary_path + return valid_binaries + + +def fill_template(src_path, token_value_dict): + result = '' + with open(src_path, 'r') as file: + src = Template(file.read()) + result = src.substitute(token_value_dict) + return result + + +def fill_template_to_file(src_path, dest_path, token_value_dict): + result = fill_template(src_path, token_value_dict) + with open(dest_path, 'w') as outfile: + outfile.write(result) + + +def fill_wix_template(houdini_versions, copied_binaries, build_dir): + wix_directories = '' + wix_features = '' + + for version in copied_binaries: + version_dict = { + 'HOU_MAJOR': houdini_versions[version]['major'], + 'HOU_MINOR': houdini_versions[version]['minor'], + 'HOU_PATCH': houdini_versions[version]['patch'] + } + newline = '' + if len(wix_directories) > 0: + newline = '\n' + wix_directories += newline + \ + fill_template(WIX_DIRECTORY_TEMPLATE_PATH, version_dict) + wix_features += newline + \ + fill_template(WIX_FEATURE_TEMPLATE_PATH, version_dict) + + wix_properties = { + 'PROJECT_NAME': PROJECT_NAME, + 'PLD_VERSION_MMP': get_palladio_version_string(), + 'PACKAGE_VENDOR_ESCAPED': PACKAGE_VENDOR_ESCAPED, + 'RESOURCE_FOLDER': RESOURCE_DIR, + 'HOUDINI_DIRECTORY': wix_directories, + 'PALLADIO_FOR_HOUDINI_FEATURE': wix_features + } + fill_template_to_file(WIX_TEMPLATE_PATH, build_dir / + 'palladio.wxs', wix_properties) + + +def fill_deployment_properties_templates(installer_filename, build_dir, build_version): + deployment_properties = { + 'PACKAGE_NAME': PROJECT_NAME, + 'PLD_VERSION_BASE': get_palladio_version_string(), + 'PLD_VERSION': get_palladio_version_string(True, build_version), + 'PACKAGE_FILE_NAME': installer_filename, + 'PLD_PKG_OS': PLATFORM + } + fill_template_to_file(DEPLOYMENT_PROPERTIES_TEMPLATE_PATH, build_dir / + 'deployment.properties', deployment_properties) + + +def parse_arguments(houdini_versions): + parser = argparse.ArgumentParser( + description='Build unified MSI installer for the Palladio Plugin') + for houdini_version in houdini_versions: + h_major = houdini_versions[houdini_version]['major'] + h_minor = houdini_versions[houdini_version]['minor'] + parser.add_argument('-h' + h_major + h_minor, '--' + houdini_version, default='', + help='path to binary location of Palladio build for ' + houdini_version) + + parser.add_argument('-bv', '--build_version', default='', + help='build version for current Palladio build') + + parser.add_argument('-bd', '--build_dir', default=str(DEFAULT_BUILD_DIR), + help='build directory where installer files are generated') + + parsed_args = vars(parser.parse_args()) + binary_folders = {} + for item in parsed_args: + if item.__contains__('houdini'): + binary_folders[item] = parsed_args[item] + return binary_folders, parsed_args['build_version'], Path(parsed_args['build_dir']).resolve() + + +def main(): + with open(CFG_PATH, 'r') as config_file: + installer_config = json.load(config_file) + + houdini_versions = installer_config['houdini_versions'] + + binary_folders, build_version, build_dir = parse_arguments(houdini_versions) + + copied_binaries = copy_binaries(binary_folders, build_dir) + installer_filename = gen_installer_filename(build_version) + + if len(copied_binaries) == 0: + print('Please provide at least one palladio binary folder, see "build.py --help" for help') + return + + fill_deployment_properties_templates( + installer_filename, build_dir, build_version) + fill_wix_template(houdini_versions, copied_binaries, build_dir) + + create_installer(houdini_versions, copied_binaries, + build_dir, installer_filename) + + +if __name__ == '__main__': + main() diff --git a/deploy/installer_config.json b/deploy/installer_config.json new file mode 100644 index 0000000..8f41be4 --- /dev/null +++ b/deploy/installer_config.json @@ -0,0 +1,19 @@ +{ + "houdini_versions":{ + "houdini18.5": { + "major": "18", + "minor": "5", + "patch": "759" + }, + "houdini19.0": { + "major": "19", + "minor": "0", + "patch": "720" + }, + "houdini19.5": { + "major": "19", + "minor": "5", + "patch": "303" + } + } +} \ No newline at end of file diff --git a/deploy/palladio.json b/deploy/palladio.json new file mode 100644 index 0000000..8a7ecb4 --- /dev/null +++ b/deploy/palladio.json @@ -0,0 +1,14 @@ +{ + "enable": true, + "env": [ + { + "PLD_PATH": [ + {"houdini_version == '18.5'": "C:/ProgramData/ESRI/Palladio/houdini18.5"}, + {"houdini_version == '19.0'": "C:/ProgramData/ESRI/Palladio/houdini19.0"}, + {"houdini_version == '19.5'": "C:/ProgramData/ESRI/Palladio/houdini19.5"} + ], + "PATH": "$PLD_PATH" + } + ], + "path": "$PLD_PATH" +} \ No newline at end of file diff --git a/deploy/resources/banner.png b/deploy/resources/banner.png new file mode 100644 index 0000000..b93821f Binary files /dev/null and b/deploy/resources/banner.png differ diff --git a/deploy/resources/dialog.png b/deploy/resources/dialog.png new file mode 100644 index 0000000..c3b253a Binary files /dev/null and b/deploy/resources/dialog.png differ diff --git a/deploy/resources/license.rtf b/deploy/resources/license.rtf new file mode 100644 index 0000000..47ee2a3 Binary files /dev/null and b/deploy/resources/license.rtf differ diff --git a/deploy/resources/palladio.ico b/deploy/resources/palladio.ico new file mode 100644 index 0000000..cf3a051 Binary files /dev/null and b/deploy/resources/palladio.ico differ diff --git a/deploy/templates/deployment.properties.in b/deploy/templates/deployment.properties.in new file mode 100644 index 0000000..850436b --- /dev/null +++ b/deploy/templates/deployment.properties.in @@ -0,0 +1,5 @@ +package_name = ${PACKAGE_NAME} +package_version_base = ${PLD_VERSION_BASE} +package_version = ${PLD_VERSION} +package_file = ${PACKAGE_FILE_NAME} +package_classifier = ${PLD_PKG_OS} \ No newline at end of file diff --git a/deploy/templates/palladio.wxs.in b/deploy/templates/palladio.wxs.in new file mode 100644 index 0000000..db7ef13 --- /dev/null +++ b/deploy/templates/palladio.wxs.in @@ -0,0 +1,52 @@ + + + + + + + + + + + + ProductIcon.ico + + + + + + + + + + ${HOUDINI_DIRECTORY} + + + + + + + + + + + + + + +${PALLADIO_FOR_HOUDINI_FEATURE} + + + + + + + + + \ No newline at end of file diff --git a/deploy/templates/palladio_directory.in b/deploy/templates/palladio_directory.in new file mode 100644 index 0000000..cd30bc0 --- /dev/null +++ b/deploy/templates/palladio_directory.in @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deploy/templates/palladio_feature.in b/deploy/templates/palladio_feature.in new file mode 100644 index 0000000..322e31a --- /dev/null +++ b/deploy/templates/palladio_feature.in @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 37c4067..fff13dd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.19) project(palladio CXX) @@ -20,10 +20,13 @@ set(HOUDINI_RELATIVE_PALLADIO_PATH "${HOUDINI_RELATIVE_PACKAGES_PATH}/palladio") ### versioning -set(PLD_VERSION_MAJOR 2) -set(PLD_VERSION_MINOR 0) -set(PLD_VERSION_PATCH 0) -set(PLD_VERSION_PRE "-beta.1") # set to empty string for final releases +file(READ "${CMAKE_CURRENT_LIST_DIR}/../version.json" VERSION_FILE) + +string(JSON PLD_VERSION_MAJOR GET ${VERSION_FILE} "PLD_VERSION_MAJOR") +string(JSON PLD_VERSION_MINOR GET ${VERSION_FILE} "PLD_VERSION_MINOR") +string(JSON PLD_VERSION_PATCH GET ${VERSION_FILE} "PLD_VERSION_PATCH") +string(JSON PLD_VERSION_PRE GET ${VERSION_FILE} "PLD_VERSION_PRE") + if (NOT PLD_VERSION_BUILD) set(PLD_VERSION_BUILD DEV) endif () diff --git a/version.json b/version.json new file mode 100644 index 0000000..ffe6128 --- /dev/null +++ b/version.json @@ -0,0 +1,6 @@ +{ + "PLD_VERSION_MAJOR": "2", + "PLD_VERSION_MINOR": "0", + "PLD_VERSION_PATCH": "0", + "PLD_VERSION_PRE": "-beta.1" +} \ No newline at end of file