diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..d05a337 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,192 @@ +stages: + - build + - deploy + - publish + +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - lib/ + +before_script: + - echo Current working directory is $(pwd) + +build linux x86_64: + image: ubuntu + stage: build + tags: + - docker + - linux + script: + # Install dependencies + - apt-get update + - > + apt-get install --no-install-recommends -y qt5-default qtmultimedia5-dev + clang make git sudo curl ca-certificates pkg-config upx unzip + + # Print versions + - qmake --version + - clang --version + + # Extract BASS + - mkdir bass + - cd bass + - curl http://www.un4seen.com/files/bass24-linux.zip -o bass.zip + - unzip bass.zip + - cp x64/libbass.so ../lib + - curl http://www.un4seen.com/files/bassopus24-linux.zip -o bassopus.zip + - unzip bassopus.zip + - cp x64/libbassopus.so ../lib + - cd .. + + # Extract Discord RPC + - mkdir discord-rpc + - cd discord-rpc + - curl -L https://github.com/discordapp/discord-rpc/releases/download/v3.4.0/discord-rpc-linux.zip -o discord_rpc_linux.zip + - unzip discord_rpc_linux.zip + - cp discord-rpc/linux-dynamic/lib/libdiscord-rpc.so ../lib + - cd .. + + # Extract QtApng + - mkdir qtapng + - cd qtapng + - curl -L https://github.com/Skycoder42/QtApng/releases/download/1.1.0-5/build_gcc_64_5.12.0.tar.xz -o apng.tar.xz + - tar -xvf apng.tar.xz + - cp gcc_64/plugins/imageformats/libqapng.so ../lib + - cd .. + + # Build + - qmake -spec linux-clang + - make -j4 + + # Post-processing + - upx --lzma -9 --force bin/Attorney_Online + artifacts: + paths: + - bin/ + +build windows i686: + image: ${CI_REGISTRY_IMAGE}/builder-windows-i686 + stage: build + tags: + - docker + - linux + script: + # Extract BASS + - mkdir bass + - cd bass + - curl http://www.un4seen.com/files/bass24.zip -o bass.zip + - unzip bass.zip + - cp bass.dll ../lib + - curl http://www.un4seen.com/files/bassopus24.zip -o bassopus.zip + - unzip bassopus.zip + - cp bassopus.dll ../lib + - cd .. + + # Build + - /opt/mxe/usr/${TARGET_SPEC}/qt5/bin/qmake + - make -j4 + + # Post-processing + - upx --lzma -9 --force bin/Attorney_Online.exe + artifacts: + paths: + - bin/ + +# Base folder +.deploy_base: &deploy_base | + mkdir base + mkdir base/themes + cp -a ../base/themes/default base/themes/ + cp -a ../base/config.ini base/config.sample.ini + cp -a ../base/serverlist.txt base/serverlist.sample.txt + +# Miscellaneous files +.deploy_misc: &deploy_misc | + cp -a ../README.md README.md.txt + cp -a ../LICENSE.MIT LICENSE.txt + +deploy linux x86_64: + stage: deploy + dependencies: + - build linux x86_64 + tags: + - docker + - linux + script: + - mkdir artifact + - cd artifact + - *deploy_base + - *deploy_misc + + # Platform-specific + - cp -a ../lib/*.so . + - cp -a ../bin/Attorney_Online . + - echo "#!/bin/sh" >> ./run.sh + - echo "LD_LIBRARY_PATH=.:\$LD_LIBRARY_PATH ./Attorney_Online" >> ./run.sh + - chmod +x ./run.sh + + # Zipping + # zip -r -9 -l Attorney_Online_$(git describe --tags)_linux_x86_64.zip . + - mkdir ../zip + - tar cavf ../zip/Attorney_Online_$(git describe --tags)_linux_x64.tar.xz * + - sha1sum ../zip/* + artifacts: + paths: + - zip/ + +deploy windows i686: + image: ubuntu + stage: deploy + dependencies: + - build windows i686 + tags: + - docker + - linux + script: + - apt-get update + - apt-get install --no-install-recommends -y zip git + + - mkdir artifact + - cd artifact + - *deploy_base + - *deploy_misc + + # Platform-specific + - cp -a ../lib/*.dll . + - cp -a ../bin/Attorney_Online.exe . + + # Zipping + # -r: recursive; -9: max compression; -l: convert to CR LF + - mkdir ../zip + - zip -r -9 -l ../zip/Attorney_Online_$(git describe --tags)_windows_x86.zip . + - sha1sum ../zip/* + artifacts: + paths: + - zip/ + +publish linux x86_64: + image: ubuntu + stage: publish + dependencies: + - deploy linux x86_64 + when: manual + script: + - cd zip + - ../scripts/wasabi.sh + variables: + MANIFEST: program_linux_x86_64.json + ARTIFACT_SUFFIX: _linux_x64.tar.xz + +publish windows i686: + image: ubuntu + stage: publish + dependencies: + - deploy windows i686 + when: manual + script: + - cd zip + - ../scripts/wasabi.sh + variables: + MANIFEST: program_winnt_i386.json + ARTIFACT_SUFFIX: _windows_x86.zip diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4243b3b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: cpp +os: osx + +addons: + homebrew: + packages: + - qt5 + +script: + - ./scripts/macos_build.sh + - ./scripts/macos_post_build.sh diff --git a/Attorney_Online.pro b/Attorney_Online.pro index c0bc4ab..e1961a0 100644 --- a/Attorney_Online.pro +++ b/Attorney_Online.pro @@ -13,6 +13,7 @@ MOC_DIR = $$PWD/build SOURCES += $$files($$PWD/src/*.cpp) HEADERS += $$files($$PWD/include/*.h) LIBS += -L$$PWD/lib -lbass -ldiscord-rpc +macx:LIBS += -framework CoreFoundation -framework Foundation -framework CoreServices CONFIG += c++14 diff --git a/include/aoapplication.h b/include/aoapplication.h index 44aef7a..e698030 100644 --- a/include/aoapplication.h +++ b/include/aoapplication.h @@ -94,9 +94,9 @@ public: //////////////////versioning/////////////// - constexpr int get_release() const { return RELEASE; } - constexpr int get_major_version() const { return MAJOR_VERSION; } - constexpr int get_minor_version() const { return MINOR_VERSION; } + int get_release() const { return RELEASE; } + int get_major_version() const { return MAJOR_VERSION; } + int get_minor_version() const { return MINOR_VERSION; } QString get_version_string(); /////////////////////////////////////////// diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..2f8a2b9 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,6 @@ +node_modules/ + +# Cursed file +package-lock.json + +s3_keys.sh diff --git a/scripts/macos_build.sh b/scripts/macos_build.sh new file mode 100755 index 0000000..22e2d45 --- /dev/null +++ b/scripts/macos_build.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +set -Eexo pipefail + +export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/local/opt/openssl/lib/pkgconfig +export PATH=$PATH:/usr/local/opt/qt5/bin:/usr/local/bin + +mkdir bass +cd bass +curl http://www.un4seen.com/files/bass24-osx.zip -o bass.zip +unzip bass.zip +cp libbass.dylib ../lib + +curl http://www.un4seen.com/files/bassopus24-osx.zip -o bassopus.zip +unzip bassopus.zip +cp libbassopus.dylib ../lib +cd .. + +mkdir discord-rpc +cd discord-rpc +curl -L https://github.com/discordapp/discord-rpc/releases/download/v3.4.0/discord-rpc-osx.zip -o discord_rpc_osx.zip +unzip discord_rpc_osx.zip +cp discord-rpc/osx-static/lib/libdiscord-rpc.a ../lib +cd .. + +mkdir qtapng +cd qtapng +curl -L https://github.com/Skycoder42/QtApng/releases/download/1.1.0-5/build_clang_64_5.12.0.tar.xz -o apng.tar.xz +tar -xvf apng.tar.xz +cp clang_64/plugins/imageformats/libqapng.dylib ../lib +cd .. + +qmake && make -j2 diff --git a/scripts/macos_post_build.sh b/scripts/macos_post_build.sh new file mode 100644 index 0000000..50acb40 --- /dev/null +++ b/scripts/macos_post_build.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# This script prepares the compiled bundle for shipping as a standalone release +# Assumes the Qt bin folder is in PATH +# Should be used on a "Release" build from QT creator +# Note that this DOES NOT add the base/ folder + +# Exit on errors and unset variables +set -eu + +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/" + +cd ${ROOT_DIR} + +# This thing basically does all the work +macdeployqt ../bin/Attorney_Online.app + +# Need to add the dependencies +cp ../lib/* ../bin/Attorney_Online.app/Contents/Frameworks + +# libbass has a funny path for some reason, just use rpath +install_name_tool -change @loader_path/libbass.dylib @rpath/libbass.dylib ../bin/Attorney_Online.app/Contents/MacOS/Attorney_Online diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..126a392 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,9 @@ +{ + "name": "ao-ci-scripts", + "version": "1.0.0", + "main": "update_manifest.js", + "dependencies": { + "argparse": "^1.0.10" + }, + "license": "ISC" +} diff --git a/scripts/update_manifest.js b/scripts/update_manifest.js new file mode 100755 index 0000000..1a06a2d --- /dev/null +++ b/scripts/update_manifest.js @@ -0,0 +1,151 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const crypto = require("crypto"); +const path = require("path"); +const ArgumentParser = require("argparse").ArgumentParser; + +function isFile(file) { + if (!fs.existsSync(file)) { + console.error(`File '${file}' not found. Try again.`); + throw Error(); + } + return file; +} + +const argParser = new ArgumentParser({ + addHelp: true, + description: "Adds a new latest version to the manifest file based on the " + + "provided zip file, including an incremental update." +}); +argParser.addArgument("manifestFile", { + metavar: "", type: isFile +}); +argParser.addArgument("version", { + metavar: "" +}); +argParser.addArgument([ "-f", "--full" ], { + metavar: "", type: isFile, nargs: 1, + dest: "fullZipFileArgs" +}); +argParser.addArgument([ "-i", "--incremental" ], { + type: isFile, nargs: 2, dest: "incrementalArgs", + metavar: ["", ""] +}); +argParser.addArgument([ "-e", "--executable" ], { + metavar: "[executable file]", nargs: 1, + dest: "executableArgs" +}); + +const { + manifestFile, + version, + fullZipFileArgs, + incrementalArgs, + executableArgs +} = argParser.parseArgs(); + +const [incrementalZipFile, changesFile] = incrementalArgs || []; +const [fullZipFile] = fullZipFileArgs || []; +const [executable] = executableArgs || []; + +// Do one final check +if (!incrementalZipFile && !fullZipFile) { + console.error("No download archive specified! Abort."); + process.exit(1); +} + +// Do a quick litmus test to prevent deleting everything incorrectly +if (changesFile && !fs.existsSync("base")) { + console.error("The working directory must be set to an " + + "asset folder in order for deleted directories " + + "to be calculated correctly. Abort."); + process.exit(1); +} + +const manifest = JSON.parse(fs.readFileSync(manifestFile)); + +const dirsDeleted = new Set(); +const specialActions = changesFile ? + fs.readFileSync(changesFile) + .toString() + .trim() + .split("\n") + .map(line => line.split("\t")) + .map(([mode, target, source]) => { + switch (mode[0]) { + case "D": // Deleted + // Check if the folder exists relative to the working + // directory, and if not, add it to the dirsDeleted list. + // Keep going up the tree to see how many directories were + // deleted. + let dir = path.dirname(target); + while (!dirsDeleted.has(dir) && !fs.existsSync(dir)) { + dirsDeleted.add(dir); + dir = path.dirname(dir); + } + + return { action: "delete", target }; + case "R": // Renamed + // NOTE: Make sure that the launcher's implementation of + // the move action also creates directories when needed. + return { action: "move", source, target}; + default: + return null; + } + }) + // Remove ignored file mode changes + .filter(action => action !== null) + // Create actions based on directories to be deleted. + // Always have deeper directories first, to guarantee that deleting + // higher-level directories will succeed. + .concat(Array.from(dirsDeleted.values()) + .sort((a, b) => b.split("/").length - a.split("/").length) + .map(dir => { + return { action: "deleteDir", target: dir }; + })) + : []; + +const urlBase = "https://s3.wasabisys.com/ao-downloads/"; + +const versionEntry = { + version, + executable, + prev: manifest.versions[0] ? manifest.versions[0].version : undefined, + full: fullZipFile ? [ + { + action: "dl", + url: urlBase + encodeURIComponent(path.basename(fullZipFile)), + hash: crypto.createHash("sha1") + .update(fs.readFileSync(fullZipFile)) + .digest("hex") + } + ] : undefined, + update: incrementalArgs ? [ + ...specialActions, + { + action: "dl", + url: urlBase + encodeURIComponent(path.basename(incrementalZipFile)), + hash: crypto.createHash("sha1") + .update(fs.readFileSync(incrementalZipFile)) + .digest("hex") + } + ] : undefined +}; + +console.log("Generated version entry:", versionEntry); + +const existingVersions = manifest.versions.filter(v => v.version == version); +if (existingVersions.length > 0) { + console.warn(`Warning: version ${version} already exists. Adding new values.`); + + // Don't overwrite prev - it will cause headaches + delete versionEntry.prev; + + Object.assign(existingVersions[0], versionEntry); + console.log("Merged version entry:", existingVersions[0]); +} else { + manifest.versions = [versionEntry, ...manifest.versions]; +} + +fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 4)); diff --git a/scripts/update_program_manifest.js b/scripts/update_program_manifest.js new file mode 100755 index 0000000..9efc814 --- /dev/null +++ b/scripts/update_program_manifest.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const crypto = require("crypto"); + +const [ _nodeExe, _jsPath, manifestFile, version, zipFile ] = process.argv; + +if (!manifestFile || !version || !zipFile) { + console.log(`Usage: update_program_manifest `); + console.log(`Adds a new latest version to the manifest file based on the ` + + `provided zip file.`); + process.exit(1); +} + +if (!fs.existsSync(manifestFile)) { + console.error(`Manifest file '${manifestFile}' not found. Try again.`); + process.exit(2); +} + +if (!fs.existsSync(zipFile)) { + console.error(`Zip file '${zipFile}' not found. Try again.`); + process.exit(2); +} + +const manifest = JSON.parse(fs.readFileSync(manifestFile)); + +manifest.versions = [{ + version, + executable: "Attorney_Online.exe", + full: [ + { + action: "dl", + url: "https://s3.wasabisys.com/ao-downloads/" + encodeURIComponent(zipFile), + hash: crypto.createHash("sha1").update(fs.readFileSync(zipFile)).digest("hex") + } + ] +}, ...manifest.versions]; + +fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 4)); \ No newline at end of file diff --git a/scripts/wasabi.sh b/scripts/wasabi.sh new file mode 100755 index 0000000..95fb3ee --- /dev/null +++ b/scripts/wasabi.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# Updates the specified program manifest to a new archive and version +# and uploads the new archive and manifest to S3/Wasabi. +# +# Requires: +# MANIFEST: name of the manifest file +# S3_ACCESS_KEY, S3_SECRET_KEY: S3 credentials +# S3_MANIFESTS, S3_ARCHIVES: S3 paths to manifests and downloads +# ARCHIVE_FULL: name of the full archive (if desired) +# ARCHIVE_INCR: name of the incremental archive (if desired) +# VERSION: name of the new version +# EXECUTABLE: name of the executable (if program manifest) + + +# -E: inherit ERR trap by shell functions +# -e: stop script on ERR trap +# -u: stop script on unbound variables +# -x: print command before running it +# -o pipefail: fail if any command in a pipeline fails +set -Eeuxo pipefail + +aws configure set aws_access_key_id ${S3_ACCESS_KEY} +aws configure set aws_secret_access_key ${S3_SECRET_KEY} +aws configure set default.region us-east-1 + +export S3_COPY="aws s3 cp --endpoint-url=https://s3.wasabisys.com" + +export ARCHIVE_FULL_ARG="" +export ARCHIVE_INCR_ARG="" +export EXECUTABLE_ARG="" + +export LAST_TAGGED_VERSION=$(git rev-list --tags --skip=1 --max-count=1) +echo "Previous tagged version: ${LAST_TAGGED_VERSION}" +echo "Current tagged version: ${VERSION}" + +if [[ -n $ARCHIVE_INCR && -n $LAST_TAGGED_VERSION ]]; then + echo "Incremental archive: ${ARCHIVE_INCR}" + + # Get all files + export CHANGES_FILE="changes.txt" + git diff --name-status ${LAST_TAGGED_VERSION}..HEAD > ${CHANGES_FILE} + + # Get added/modified files + git diff --name-only --diff-filter=dr ${LAST_TAGGED_VERSION}..HEAD | \ + zip ${ARCHIVE_INCR} -@ + + export ARCHIVE_INCR_ARG="-i ${ARCHIVE_INCR} ${CHANGES_FILE}" +elif [[ -n $ARCHIVE_INCR && -z $LAST_TAGGED_VERSION ]]; then + echo "Incremental archive was requested, but there is no previous version" +fi + +if [[ -n $ARCHIVE_FULL ]]; then + echo "Full archive: ${ARCHIVE_INCR}" + export ARCHIVE_FULL_ARG="-f ${ARCHIVE_FULL}" +fi + +if [[ -v EXECUTABLE ]]; then + export EXECUTABLE_ARG="-e ${EXECUTABLE}" +fi + +${S3_COPY} ${S3_MANIFESTS}/${MANIFEST} . +node $(dirname $0)/update_manifest.js ${MANIFEST} ${VERSION} \ + ${ARCHIVE_FULL_ARG} ${ARCHIVE_INCR_ARG} ${EXECUTABLE_ARG} + +if [[ -n $ARCHIVE_INCR_ARG ]]; then + ${S3_COPY} ${ARCHIVE_INCR} ${S3_ARCHIVES} +fi + +if [[ -n $ARCHIVE_FULL_ARG ]]; then + ${S3_COPY} ${ARCHIVE_FULL} ${S3_ARCHIVES} +fi + +${S3_COPY} ${MANIFEST} ${S3_MANIFESTS} diff --git a/scripts/wasabi_program.sh b/scripts/wasabi_program.sh new file mode 100755 index 0000000..41e2e35 --- /dev/null +++ b/scripts/wasabi_program.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Updates the specified program manifest to a new archive and version +# and uploads the new archive and manifest to S3/Wasabi. +# +# Requires: +# MANIFEST: name of the manifest file +# ARTIFACT_SUFFIX: suffix of the archive to be uploaded (including extension) +# S3_ACCESS_KEY and S3_SECRET_KEY + + +# -E: inherit ERR trap by shell functions +# -e: stop script on ERR trap +# -u: stop script on unbound variables +# -x: print command before running it +# -o pipefail: fail if any command in a pipeline fails +set -Eeuxo pipefail + +aws configure set aws_access_key_id ${S3_ACCESS_KEY} +aws configure set aws_secret_access_key ${S3_SECRET_KEY} +aws configure set default.region us-east-1 + +export S3_COPY="aws s3 cp --endpoint-url=https://s3.wasabisys.com" +export S3_MANIFESTS="s3://ao-manifests" +export S3_ARCHIVES="s3://ao-downloads" + +export VERSION=$(git describe --tags) +export ARCHIVE="Attorney_Online_${VERSION}_${ARTIFACT_SUFFIX}" + +${S3_COPY} ${S3_MANIFESTS}/${MANIFEST} . +node $(dirname $0)/update_manifest.js ${MANIFEST} ${VERSION} \ + -f ${ARCHIVE} -e Attorney_Online.exe +${S3_COPY} ${ARCHIVE} ${S3_ARCHIVES} +${S3_COPY} ${MANIFEST} ${S3_MANIFESTS} + +rm -f ${MANIFEST} diff --git a/scripts/windows/Dockerfile b/scripts/windows/Dockerfile new file mode 100644 index 0000000..90d6c27 --- /dev/null +++ b/scripts/windows/Dockerfile @@ -0,0 +1,17 @@ +FROM oldmud0/mxe-qt:5.12.1-win32-static-posix +#FROM fffaraz/qt:windows + +ENV TARGET_SPEC i686-w64-mingw32.static.posix + +# Build Discord RPC statically +RUN git clone https://github.com/discordapp/discord-rpc +WORKDIR discord-rpc/build +RUN /opt/mxe/usr/bin/${TARGET_SPEC}-cmake .. -DCMAKE_INSTALL_PREFIX=/opt/mxe/usr/${TARGET_SPEC} +RUN /opt/mxe/usr/bin/${TARGET_SPEC}-cmake --build . --config Release --target install +WORKDIR ../.. + +# Build QtApng statically +RUN git clone https://github.com/Skycoder42/QtApng +WORKDIR QtApng +RUN /opt/mxe/usr/${TARGET_SPEC}/qt5/bin/qmake && make qmake_all && make && make install +WORKDIR .. diff --git a/scripts/windows/Dockerfile-mxe b/scripts/windows/Dockerfile-mxe new file mode 100644 index 0000000..e6caec5 --- /dev/null +++ b/scripts/windows/Dockerfile-mxe @@ -0,0 +1,44 @@ +FROM ubuntu:18.04 + +RUN apt-get update +RUN apt-get install -y \ + autoconf \ + automake \ + autopoint \ + bash \ + bison \ + bzip2 \ + flex \ + g++ \ + g++-multilib \ + gettext \ + git \ + gperf \ + intltool \ + libc6-dev-i386 \ + libgdk-pixbuf2.0-dev \ + libltdl-dev \ + libssl-dev \ + libtool-bin \ + libxml-parser-perl \ + lzip \ + make \ + openssl \ + p7zip-full \ + patch \ + perl \ + pkg-config \ + python \ + ruby \ + sed \ + unzip \ + wget \ + xz-utils + +RUN git clone https://github.com/mxe/mxe.git +RUN mv mxe /opt/mxe +WORKDIR /opt/mxe +RUN make -j4 MXE_TARGETS="i686-w64-mingw32.static.posix" qtbase qtmultimedia +ENV PATH=/opt/mxe/usr/bin:$PATH + +WORKDIR / diff --git a/scripts/windows/how-to-push.md b/scripts/windows/how-to-push.md new file mode 100644 index 0000000..8c1c18d --- /dev/null +++ b/scripts/windows/how-to-push.md @@ -0,0 +1,19 @@ +When you want to build a new version of Qt: +```docker +docker build -t mxe-windows-static . -f Dockerfile-mxe +docker tag mxe-windows-static oldmud0/mxe-qt:5.12.1-win32-static-posix +docker push oldmud0/mxe-qt:5.12.1-win32-static-posix +``` + +Remember to log into Docker Hub before attempting to push. + +When you want to build a new version of any dependency required for building AO: +```docker +docker build -t mxe-windows-static-ao . -f Dockerfile +docker tag mxe-windows-static-ao registry.gitlab.com/attorneyonline/ao2-client/builder-windows-i686 +docker push registry.gitlab.com/attorneyonline/ao2-client/builder-windows-i686 +``` + +Remember to create an access token in GitLab before attempting to push. + +GitLab CI depends on `builder-windows-i686` image to be present in the repository's registry in order for the Windows build to succeed.