diff --git a/.github/FUNDING.yml b/.github/funding.yml similarity index 100% rename from .github/FUNDING.yml rename to .github/funding.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index bd839e3..ff761f1 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -14,6 +14,7 @@ jobs: sabnzbd scripts tools + builder tests --line-length=120 --target-version=py36 diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml new file mode 100644 index 0000000..ed84900 --- /dev/null +++ b/.github/workflows/build_release.yml @@ -0,0 +1,76 @@ +name: Build binaries and source distribution + +on: [push, pull_request] + +jobs: + build_windows: + name: Build ${{ matrix.name }} binary + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - name: macOS + os: macos-latest + python-version: 3.9 + - name: Windows + os: windows-latest + python-version: 3.9 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python dependencies + run: | + python --version + pip install --upgrade pip wheel + pip install --upgrade -r requirements.txt + pip install --upgrade -r builder/requirements.txt + - name: Build source distribution + if: runner.os == 'Windows' + run: python builder/package.py source + - name: Upload source distribution + if: runner.os == 'Windows' + uses: actions/upload-artifact@v2 + with: + path: "*-src.tar.gz" + name: Source distribution + - name: Build Windows standalone binary and installer + if: runner.os == 'Windows' + run: python builder/package.py installer + - name: Upload Windows standalone binary (64bit) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v2 + with: + path: "*-win64-bin.zip" + name: Windows Windows standalone binary (64bit) + - name: Upload Windows installer + if: runner.os == 'Windows' + uses: actions/upload-artifact@v2 + with: + path: "*-win-setup.exe" + name: Windows installer + - name: Import codesign certificates + uses: apple-actions/import-codesign-certs@v1 + if: runner.os == 'macOS' + with: + p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} + p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} + - name: Build macOS binary + if: runner.os == 'macOS' + run: | + python builder/package.py app + python builder/make_dmg.py + env: + SIGNING_AUTH: ${{ secrets.SIGNING_AUTH }} + - name: Upload macOS binary + if: runner.os == 'macOS' + uses: actions/upload-artifact@v2 + with: + path: "*-osx.dmg" + name: macOS binary (not notarized) + + + diff --git a/.github/workflows/integration_testing.yml b/.github/workflows/integration_testing.yml index 902fbd3..92bcb43 100644 --- a/.github/workflows/integration_testing.yml +++ b/.github/workflows/integration_testing.yml @@ -4,16 +4,18 @@ on: [push, pull_request] jobs: test: - name: Test ${{ matrix.os }} - Python ${{ matrix.python-version }} + name: Test ${{ matrix.name }} - Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] os: [ubuntu-20.04] include: - - os: macos-latest + - name: macOS + os: macos-latest python-version: 3.9 - - os: windows-latest + - name: Windows + os: windows-latest python-version: 3.9 steps: diff --git a/builder/SABnzbd.spec b/builder/SABnzbd.spec new file mode 100644 index 0000000..40f2306 --- /dev/null +++ b/builder/SABnzbd.spec @@ -0,0 +1,184 @@ +# -*- mode: python -*- +import re +import sys +import pkginfo + +from PyInstaller.building.api import EXE, COLLECT, PYZ +from PyInstaller.building.build_main import Analysis +from PyInstaller.building.osx import BUNDLE + +# Add extra files in the PyInstaller-spec +extra_pyinstaller_files = [] + +# Also modify these in "package.py"! +extra_files = [ + "README.txt", + "INSTALL.txt", + "LICENSE.txt", + "GPL2.txt", + "GPL3.txt", + "COPYRIGHT.txt", + "ISSUES.txt", + "PKG-INFO", +] + +extra_folders = [ + "scripts/", + "licenses/", + "locale/", + "email/", + "interfaces/Plush/", + "interfaces/Glitter/", + "interfaces/wizard/", + "interfaces/Config/", + "scripts/", + "icons/", +] + +# Get the version +RELEASE_VERSION = pkginfo.Develop(".").version + +# Add hidden imports +extra_hiddenimports = ["Cheetah.DummyTransaction", "cheroot.ssl.builtin", "certifi"] + +# Add platform specific stuff +if sys.platform == "darwin": + extra_hiddenimports.extend(["pyobjc", "objc", "PyObjCTools"]) + # macOS folders + extra_folders += ["osx/par2/", "osx/unrar/", "osx/7zip/"] + # Add NZB-icon file + extra_pyinstaller_files.append(("builder/osx/image/nzbfile.icns", ".")) + # Version information is set differently on macOS + version_info = None +else: + # Build would fail on non-Windows + from PyInstaller.utils.win32.versioninfo import ( + VSVersionInfo, + FixedFileInfo, + StringFileInfo, + StringTable, + StringStruct, + VarFileInfo, + VarStruct, + ) + + # Windows + extra_hiddenimports.append("win32timezone") + extra_folders += ["win/multipar/", "win/unrar/", "win/7zip/"] + + # Parse the version info + version_regexed = re.search(r"(\d+)\.(\d+)\.(\d+)([a-zA-Z]*)(\d*)", RELEASE_VERSION) + version_tuple = (int(version_regexed.group(1)), int(version_regexed.group(2)), int(version_regexed.group(3)), 0) + + # Detailed instructions are in the PyInstaller documentation + # We don't include the alpha/beta/rc in the counters + version_info = VSVersionInfo( + ffi=FixedFileInfo( + filevers=version_tuple, + prodvers=version_tuple, + mask=0x3F, + flags=0x0, + OS=0x40004, + fileType=0x1, + subtype=0x0, + date=(0, 0), + ), + kids=[ + StringFileInfo( + [ + StringTable( + "040904B0", + [ + StringStruct("Comments", f"SABnzbd {RELEASE_VERSION}"), + StringStruct("CompanyName", "The SABnzbd-Team"), + StringStruct("FileDescription", f"SABnzbd {RELEASE_VERSION}"), + StringStruct("FileVersion", RELEASE_VERSION), + StringStruct("LegalCopyright", "The SABnzbd-Team"), + StringStruct("ProductName", f"SABnzbd {RELEASE_VERSION}"), + StringStruct("ProductVersion", RELEASE_VERSION), + ], + ) + ] + ), + VarFileInfo([VarStruct("Translation", [1033, 1200])]), + ], + ) + +# Process the extra-files and folders +for file_item in extra_files: + extra_pyinstaller_files.append((file_item, ".")) +for folder_item in extra_folders: + extra_pyinstaller_files.append((folder_item, folder_item)) + +pyi_analysis = Analysis( + ["SABnzbd.py"], + datas=extra_pyinstaller_files, + hiddenimports=extra_hiddenimports, + excludes=["FixTk", "tcl", "tk", "_tkinter", "tkinter", "Tkinter"], +) + +pyz = PYZ(pyi_analysis.pure, pyi_analysis.zipped_data) + +exe = EXE( + pyz, + pyi_analysis.scripts, + [], + exclude_binaries=True, + name="SABnzbd", + upx=True, + console=False, + append_pkg=False, + icon="icons/sabnzbd.ico", + version=version_info, +) + +coll = COLLECT(exe, pyi_analysis.binaries, pyi_analysis.zipfiles, pyi_analysis.datas, name="SABnzbd") + +# We need to run again for the console-app +if sys.platform == "win32": + # Enable console=True for this one + console_exe = EXE( + pyz, + pyi_analysis.scripts, + [], + exclude_binaries=True, + name="SABnzbd-console", + upx=True, + append_pkg=False, + icon="icons/sabnzbd.ico", + version=version_info, + ) + + console_coll = COLLECT( + console_exe, + pyi_analysis.binaries, + pyi_analysis.zipfiles, + pyi_analysis.datas, + upx=True, + name="SABnzbd-console", + ) + +# Build the APP on macOS +if sys.platform == "darwin": + info_plist = { + "NSUIElement": 1, + "NSPrincipalClass": "NSApplication", + "CFBundleShortVersionString": RELEASE_VERSION, + "NSHumanReadableCopyright": "The SABnzbd-Team", + "CFBundleIdentifier": "org.sabnzbd.sabnzbd", + "CFBundleDocumentTypes": [ + { + "CFBundleTypeExtensions": ["nzb"], + "CFBundleTypeIconFile": "nzbfile.icns", + "CFBundleTypeMIMETypes": ["text/nzb"], + "CFBundleTypeName": "NZB File", + "CFBundleTypeRole": "Viewer", + "LSTypeIsPackage": 0, + "NSPersistentStoreTypeKey": "Binary", + } + ], + "LSMinimumSystemVersion": "10.9", + "LSEnvironment": {"LANG": "en_US.UTF-8", "LC_ALL": "en_US.UTF-8"}, + } + + app = BUNDLE(coll, name="SABnzbd.app", icon="builder/osx/image/sabnzbdplus.icns", info_plist=info_plist) diff --git a/builder/make_dmg.py b/builder/make_dmg.py new file mode 100644 index 0000000..3cf1952 --- /dev/null +++ b/builder/make_dmg.py @@ -0,0 +1,206 @@ +#!/usr/bin/python3 -OO +# Copyright 2008-2017 The SABnzbd-Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import os +import pkginfo + + +# We need to call dmgbuild from command-line, so here we can setup how +if __name__ == "__main__": + # Check for DMGBuild + try: + import dmgbuild + except: + print("Requires dmgbuild-module, use pip install dmgbuild") + exit() + + # Make sure we are in the src folder + if not os.path.exists("builder"): + raise FileNotFoundError("Run from the main SABnzbd source folder: python builder/package.py") + + # Check if signing is possible + authority = os.environ.get("SIGNING_AUTH") + + # Extract version info and set DMG path + # Create sub-folder to upload later + release = pkginfo.Develop(".").version + prod = "SABnzbd-" + release + fileDmg = prod + "-osx.dmg" + + # Path to app file + apppath = "dist/SABnzbd.app" + + # Copy Readme + readmepath = os.path.join(apppath, "Contents/Resources/README.txt") + + # Path to background and the icon + backgroundpath = "builder/osx/image/sabnzbd_new_bg.png" + iconpath = "builder/osx/image/sabnzbdplus.icns" + + # Make DMG + print("Building DMG") + dmgbuild.build_dmg( + filename=fileDmg, + volume_name=prod, + settings_file="builder/make_dmg.py", + defines={"app": apppath, "readme": readmepath, "background": backgroundpath, "iconpath": iconpath}, + ) + + # Resign APP + if authority: + print("Siging DMG") + os.system('codesign --deep -f -i "org.sabnzbd.SABnzbd" -s "%s" "%s"' % (authority, fileDmg)) + print("Signed!") + else: + print("Signing skipped, missing SIGNING_AUTH.") + exit() + + +### START OF DMGBUILD SETTINGS +### COPIED AND MODIFIED FROM THE EXAMPLE ONLINE +application = defines.get("app", "AppName.app") +readme = defines.get("readme", "ReadMe.rtf") +appname = os.path.basename(application) + +# .. Basics .................................................................... + +# Volume format (see hdiutil create -help) +format = defines.get("format", "UDBZ") + +# Volume size (must be large enough for your files) +size = defines.get("size", "100M") + +# Files to include +files = [application, readme] + +# Symlinks to create +symlinks = {"Applications": "/Applications"} + +# Volume icon +# +# You can either define icon, in which case that icon file will be copied to the +# image, *or* you can define badge_icon, in which case the icon file you specify +# will be used to badge the system's Removable Disk icon +# +badge_icon = defines.get("iconpath", "") + +# Where to put the icons +icon_locations = {readme: (70, 160), appname: (295, 220), "Applications": (510, 220)} + +# .. Window configuration ...................................................... + +# Window position in ((x, y), (w, h)) format +window_rect = ((100, 100), (660, 360)) + +# Background +# +# This is a STRING containing any of the following: +# +# #3344ff - web-style RGB color +# #34f - web-style RGB color, short form (#34f == #3344ff) +# rgb(1,0,0) - RGB color, each value is between 0 and 1 +# hsl(120,1,.5) - HSL (hue saturation lightness) color +# hwb(300,0,0) - HWB (hue whiteness blackness) color +# cmyk(0,1,0,0) - CMYK color +# goldenrod - X11/SVG named color +# builtin-arrow - A simple built-in background with a blue arrow +# /foo/bar/baz.png - The path to an image file +# +# Other color components may be expressed either in the range 0 to 1, or +# as percentages (e.g. 60% is equivalent to 0.6). +background = defines.get("background", "builtin-arrow") + +show_status_bar = False +show_tab_view = False +show_toolbar = False +show_pathbar = False +show_sidebar = False +sidebar_width = 0 + +# Select the default view; must be one of +# +# 'icon-view' +# 'list-view' +# 'column-view' +# 'coverflow' +# +default_view = "icon-view" + +# General view configuration +show_icon_preview = False + +# Set these to True to force inclusion of icon/list view settings (otherwise +# we only include settings for the default view) +include_icon_view_settings = "auto" +include_list_view_settings = "auto" + +# .. Icon view configuration ................................................... + +arrange_by = None +grid_offset = (0, 0) +grid_spacing = 50 +scroll_position = (0, 0) +label_pos = "bottom" # or 'right' +text_size = 16 +icon_size = 64 + +# .. List view configuration ................................................... + +# Column names are as follows: +# +# name +# date-modified +# date-created +# date-added +# date-last-opened +# size +# kind +# label +# version +# comments +# +list_icon_size = 16 +list_text_size = 12 +list_scroll_position = (0, 0) +list_sort_by = "name" +list_use_relative_dates = True +list_calculate_all_sizes = (False,) +list_columns = ("name", "date-modified", "size", "kind", "date-added") +list_column_widths = { + "name": 300, + "date-modified": 181, + "date-created": 181, + "date-added": 181, + "date-last-opened": 181, + "size": 97, + "kind": 115, + "label": 100, + "version": 75, + "comments": 300, +} +list_column_sort_directions = { + "name": "ascending", + "date-modified": "descending", + "date-created": "descending", + "date-added": "descending", + "date-last-opened": "descending", + "size": "descending", + "kind": "ascending", + "label": "ascending", + "version": "ascending", + "comments": "ascending", +} diff --git a/builder/osx/entitlements.plist b/builder/osx/entitlements.plist new file mode 100644 index 0000000..92c3fd7 --- /dev/null +++ b/builder/osx/entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + \ No newline at end of file diff --git a/builder/osx/image/nzbfile.icns b/builder/osx/image/nzbfile.icns new file mode 100644 index 0000000..af93677 Binary files /dev/null and b/builder/osx/image/nzbfile.icns differ diff --git a/builder/osx/image/sabnzbd_new_bg.png b/builder/osx/image/sabnzbd_new_bg.png new file mode 100644 index 0000000..4885ea1 Binary files /dev/null and b/builder/osx/image/sabnzbd_new_bg.png differ diff --git a/builder/osx/image/sabnzbd_new_bg.psd b/builder/osx/image/sabnzbd_new_bg.psd new file mode 100644 index 0000000..9e1d390 Binary files /dev/null and b/builder/osx/image/sabnzbd_new_bg.psd differ diff --git a/builder/osx/image/sabnzbdplus.icns b/builder/osx/image/sabnzbdplus.icns new file mode 100644 index 0000000..b307d37 Binary files /dev/null and b/builder/osx/image/sabnzbdplus.icns differ diff --git a/builder/package.py b/builder/package.py new file mode 100644 index 0000000..dca59b5 --- /dev/null +++ b/builder/package.py @@ -0,0 +1,545 @@ +#!/usr/bin/python3 -OO +# Copyright 2008-2017 The SABnzbd-Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import glob +import platform +import re +import sys +import os +import time +import shutil +import subprocess +import tarfile +import pkginfo +import github +from distutils.dir_util import copy_tree + + +VERSION_FILE = "sabnzbd/version.py" +SPEC_FILE = "SABnzbd.spec" + +# Also modify these in "SABnzbd.spec"! +extra_files = [ + "README.mkd", + "INSTALL.txt", + "LICENSE.txt", + "GPL2.txt", + "GPL3.txt", + "COPYRIGHT.txt", + "ISSUES.txt", + "PKG-INFO", +] + +extra_folders = [ + "scripts/", + "licenses/", + "locale/", + "email/", + "interfaces/Plush/", + "interfaces/Glitter/", + "interfaces/wizard/", + "interfaces/Config/", + "scripts/", + "icons/", +] + + +# Support functions +def safe_remove(path): + """Remove file without erros if the file doesn't exist + Can also handle folders + """ + if os.path.exists(path): + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + +def delete_files_glob(name): + """ Delete one file or set of files from wild-card spec """ + for f in glob.glob(name): + if os.path.exists(f): + os.remove(f) + + +def run_external_command(command): + """ Wrapper to ease the use of calling external programs """ + process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output, _ = process.communicate() + ret = process.wait() + if output: + print(output) + if ret != 0: + raise RuntimeError("Command returned non-zero exit code %s!" % ret) + return output + + +def run_git_command(parms): + """ Run git command, raise error if it failed """ + return run_external_command(["git"] + parms) + + +def patch_version_file(release_name): + """Patch in the Git commit hash, but only when this is + an unmodified checkout + """ + git_output = run_git_command(["log", "-1"]) + for line in git_output.split("\n"): + if "commit " in line: + commit = line.split(" ")[1].strip() + break + else: + raise TypeError("Commit hash not found") + + with open(VERSION_FILE, "r") as ver: + version_file = ver.read() + + version_file = re.sub(r'__baseline__\s*=\s*"[^"]*"', '__baseline__ = "%s"' % commit, version_file) + version_file = re.sub(r'__version__\s*=\s*"[^"]*"', '__version__ = "%s"' % release_name, version_file) + + with open(VERSION_FILE, "w") as ver: + ver.write(version_file) + + +if __name__ == "__main__": + # Was any option supplied? + if len(sys.argv) < 2: + raise TypeError("Please specify what to do") + + # Make sure we are in the src folder + if not os.path.exists("builder"): + raise FileNotFoundError("Run from the main SABnzbd source folder: python builder/package.py") + + # Extract version info + RELEASE_VERSION = pkginfo.Develop(".").version + + # Check if we have the needed certificates + try: + import certifi + except ImportError: + raise FileNotFoundError("Need certifi module") + + # Define release name + RELEASE_NAME = "SABnzbd-%s" % RELEASE_VERSION + RELEASE_TITLE = "SABnzbd %s" % RELEASE_VERSION + RELEASE_SRC = RELEASE_NAME + "-src.tar.gz" + RELEASE_BINARY_32 = RELEASE_NAME + "-win32-bin.zip" + RELEASE_BINARY_64 = RELEASE_NAME + "-win64-bin.zip" + RELEASE_INSTALLER = RELEASE_NAME + "-win-setup.exe" + RELEASE_MACOS = RELEASE_NAME + "-osx.dmg" + RELEASE_README = "README.mkd" + + # Patch release file + patch_version_file(RELEASE_VERSION) + + # Rename release notes file + safe_remove("README.txt") + shutil.copyfile(RELEASE_README, "README.txt") + + # Compile translations + if not os.path.exists("locale"): + run_external_command([sys.executable, "tools/make_mo.py"]) + + # Check again if translations exist, fail otherwise + if not os.path.exists("locale"): + raise FileNotFoundError("Failed to compile language files") + + # Make sure we remove any existing build-folders + safe_remove("build") + safe_remove("dist") + safe_remove(RELEASE_NAME) + + # Copy the specification + shutil.copyfile("builder/%s" % SPEC_FILE, SPEC_FILE) + + if "binary" in sys.argv or "installer" in sys.argv: + # Must be run on Windows + if sys.platform != "win32": + raise RuntimeError("Binary should be created on Windows") + + # Check what architecture we are on + RELEASE_BINARY = RELEASE_BINARY_32 + if platform.architecture()[0] == "64bit": + RELEASE_BINARY = RELEASE_BINARY_64 + + # Remove any leftovers + safe_remove(RELEASE_BINARY) + + # Run PyInstaller and check output + run_external_command([sys.executable, "-O", "-m", "PyInstaller", "SABnzbd.spec"]) + + # Use special distutils function to merge the main and console directories + copy_tree("dist/SABnzbd-console", "dist/SABnzbd") + safe_remove("dist/SABnzbd-console") + + # Remove unwanted DLL's + delete_files_glob("dist/SABnzbd/api-ms-win*.dll") + delete_files_glob("dist/SABnzbd/mfc140u.dll") + delete_files_glob("dist/SABnzbd/ucrtbase.dll") + + # Remove other files we don't need + delete_files_glob("dist/SABnzbd/PKG-INFO") + delete_files_glob("dist/SABnzbd/win32ui.pyd") + delete_files_glob("dist/SABnzbd/winxpgui.pyd") + + if "installer" in sys.argv: + # Needs to be run on 64 bit + if RELEASE_BINARY != RELEASE_BINARY_64: + raise RuntimeError("Installer should be created on 64bit Python") + + # Compile NSIS translations + safe_remove("NSIS_Installer.nsi") + safe_remove("NSIS_Installer.nsi.tmp") + shutil.copyfile("builder/win/NSIS_Installer.nsi", "NSIS_Installer.nsi") + run_external_command([sys.executable, "tools/make_mo.py", "nsis"]) + + # Remove 32bit external executables + delete_files_glob("dist/SABnzbd/win/par2/multipar/par2j.exe") + delete_files_glob("dist/SABnzbd/win/unrar/UnRAR.exe") + + # Run NSIS to build installer + run_external_command( + [ + "makensis.exe", + "/V3", + "/DSAB_PRODUCT=%s" % RELEASE_NAME, + "/DSAB_VERSION=%s" % RELEASE_VERSION, + "/DSAB_FILE=%s" % RELEASE_INSTALLER, + "NSIS_Installer.nsi.tmp", + ] + ) + + # Rename the folder + os.rename("dist/SABnzbd", RELEASE_NAME) + + # Create the archive + run_external_command(["win/7zip/7za.exe", "a", RELEASE_BINARY, RELEASE_NAME]) + + if "app" in sys.argv: + # Must be run on macOS + if sys.platform != "darwin": + raise RuntimeError("App should be created on macOS") + + # Who will sign and notarize this? + authority = os.environ.get("SIGNING_AUTH") + notarization_user = os.environ.get("NOTARIZATION_USER") + notarization_pass = os.environ.get("NOTARIZATION_PASS") + + # Run PyInstaller and check output + run_external_command([sys.executable, "-O", "-m", "PyInstaller", "SABnzbd.spec"]) + + # Only continue if we can sign + if authority: + files_to_sign = [ + "dist/SABnzbd.app/Contents/MacOS/osx/par2/par2-sl64", + "dist/SABnzbd.app/Contents/MacOS/osx/7zip/7za", + "dist/SABnzbd.app/Contents/MacOS/osx/unrar/unrar", + "dist/SABnzbd.app/Contents/MacOS/SABnzbd", + "dist/SABnzbd.app", + ] + + for file_to_sign in files_to_sign: + print("Signing %s with hardended runtime" % file_to_sign) + run_external_command( + [ + "codesign", + "--deep", + "--force", + "--timestamp", + "--options", + "runtime", + "--entitlements", + "builder/osx/entitlements.plist", + "-i", + "org.sabnzbd.sabnzbd", + "-s", + authority, + file_to_sign, + ], + ) + print("Signed %s!" % file_to_sign) + + # Only notarize for real builds that we want to deploy + if notarization_user and notarization_pass and "release" in os.environ.get("TRAVIS_TAG", ""): + # Prepare zip to upload to notarization service + print("Creating zip to send to Apple notarization service") + # We need to use ditto, otherwise the signature gets lost! + notarization_zip = RELEASE_NAME + ".zip" + run_external_command( + ["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", "dist/SABnzbd.app", notarization_zip] + ) + + # Upload to Apple + print("Sending zip to Apple notarization service") + upload_process = run_external_command( + [ + "xcrun", + "altool", + "--notarize-app", + "-t", + "osx", + "-f", + notarization_zip, + "--primary-bundle-id", + "org.sabnzbd.sabnzbd", + "-u", + notarization_user, + "-p", + notarization_pass, + ], + ) + + # Extract the notarization ID + m = re.match(".*RequestUUID = (.*?)\n", upload_process, re.S) + if not m: + raise RuntimeError("No UUID created") + uuid = m.group(1) + + print("Checking notarization of UUID: %s (every 30 seconds)" % uuid) + notarization_in_progress = True + while notarization_in_progress: + time.sleep(30) + check_status = run_external_command( + [ + "xcrun", + "altool", + "--notarization-info", + uuid, + "-u", + notarization_user, + "-p", + notarization_pass, + ], + ) + notarization_in_progress = "Status: in progress" in check_status + + # Check if success + if "Status: success" not in check_status: + raise RuntimeError("Failed to notarize..") + + # Staple the notarization! + print("Approved! Stapling the result to the app") + run_external_command(["xcrun", "stapler", "staple", "dist/SABnzbd.app"]) + elif notarization_user and notarization_pass: + print("Notarization skipped, include 'release' in the name of the tag to trigger notarization!") + else: + print("Notarization skipped, NOTARIZATION_USER or NOTARIZATION_PASS missing.") + else: + print("Signing skipped, missing SIGNING_AUTH.") + + if "source" in sys.argv: + # Prepare Source distribution package. + # We assume the sources are freshly cloned from the repo + # Make sure all source files are Unix format + src_folder = "srcdist" + safe_remove(src_folder) + os.mkdir(src_folder) + + # Remove any leftovers + safe_remove(RELEASE_SRC) + + # Add extra files and folders need for source dist + extra_folders.extend(["sabnzbd/", "po/", "linux/", "tools/", "tests/"]) + extra_files.extend(["SABnzbd.py", "requirements.txt"]) + + # Copy all folders and files to the new folder + for source_folder in extra_folders: + copy_tree(source_folder, os.path.join(src_folder, source_folder)) + + # Copy all files + for source_file in extra_files: + shutil.copyfile(source_file, os.path.join(src_folder, source_file)) + + # Make sure all line-endings are correct + for input_filename in glob.glob("%s/**/*.*" % src_folder, recursive=True): + base, ext = os.path.splitext(input_filename) + if ext.lower() not in (".py", ".txt", ".css", ".js", ".tmpl", ".sh", ".cmd"): + continue + print(input_filename) + + with open(input_filename, "rb") as input_data: + data = input_data.read() + data = data.replace(b"\r", b"") + with open(input_filename, "wb") as output_data: + output_data.write(data) + + # Create tar.gz file for source distro + with tarfile.open(RELEASE_SRC, "w:gz") as tar_output: + for root, dirs, files in os.walk(src_folder): + for _file in files: + input_path = os.path.join(root, _file) + if sys.platform == "win32": + tar_path = input_path.replace("srcdist\\", RELEASE_NAME + "/").replace("\\", "/") + else: + tar_path = input_path.replace("srcdist/", RELEASE_NAME + "/") + tarinfo = tar_output.gettarinfo(input_path, tar_path) + tarinfo.uid = 0 + tarinfo.gid = 0 + if _file in ("SABnzbd.py", "Sample-PostProc.sh", "make_mo.py", "msgfmt.py"): + # Force Linux/OSX scripts as executable + tarinfo.mode = 0o755 + else: + tarinfo.mode = 0o644 + + with open(input_path, "rb") as f: + tar_output.addfile(tarinfo, f) + + # Remove source folder + safe_remove(src_folder) + + # Release to github + if "release" in sys.argv: + # Check if tagged as release + if "release" in os.environ.get("TRAVIS_TAG", "") + os.environ.get("APPVEYOR_REPO_TAG_NAME", ""): + # Check for token + gh_token = os.environ.get("GITHUB_TOKEN", "") + if gh_token: + gh_obj = github.Github(gh_token) + gh_repo = gh_obj.get_repo("sabnzbd/sabnzbd") + + # Read the release notes + with open(RELEASE_README, "r") as readme_file: + readme_data = readme_file.read() + + # Pre-releases are longer than 6 characters (e.g. 3.1.0Beta1 vs 3.1.0, but also 3.0.11) + prerelease = len(RELEASE_VERSION) > 5 + + # We have to manually check if we already created this release + for release in gh_repo.get_releases(): + if release.tag_name == RELEASE_VERSION: + gh_release = release + print("Found existing release %s" % gh_release.title) + break + else: + # Did not find it, so create the release + print("Creating GitHub release SABnzbd %s" % RELEASE_VERSION) + gh_release = gh_repo.create_git_release( + tag=RELEASE_VERSION, name=RELEASE_TITLE, message=readme_data, draft=True, prerelease=prerelease + ) + + # Fetch existing assets, as overwriting is not allowed by GitHub + gh_assets = gh_release.get_assets() + + # Upload the assets + files_to_check = ( + RELEASE_SRC, + RELEASE_BINARY_32, + RELEASE_BINARY_64, + RELEASE_INSTALLER, + RELEASE_MACOS, + RELEASE_README, + ) + for file_to_check in files_to_check: + if os.path.exists(file_to_check): + # Check if this file was previously uploaded + for gh_asset in gh_assets: + if gh_asset.name == file_to_check: + print("Removing existing asset %s for release %s" % (gh_asset.name, gh_release.title)) + gh_asset.delete_asset() + # Upload the new one + print("Uploading %s to release %s" % (file_to_check, gh_release.title)) + gh_release.upload_asset(file_to_check) + + # Update the website + gh_repo_web = gh_obj.get_repo("sabnzbd/sabnzbd.github.io") + # Check if the branch already exists, only create one if it doesn't + skip_website_update = False + try: + gh_repo_web.get_branch(RELEASE_VERSION) + print("Branch %s on sabnzbd/sabnzbd.github.io already exists, skipping update" % RELEASE_VERSION) + skip_website_update = True + except github.GithubException: + # Create a new branch to have the changes + sb = gh_repo_web.get_branch("master") + print("Creating branch %s on sabnzbd/sabnzbd.github.io" % RELEASE_VERSION) + new_branch = gh_repo_web.create_git_ref(ref="refs/heads/" + RELEASE_VERSION, sha=sb.commit.sha) + + # Update the files + if not skip_website_update: + # We need bytes version to interact with GitHub + RELEASE_VERSION_BYTES = RELEASE_VERSION.encode() + + # Get all the version files + latest_txt = gh_repo_web.get_contents("latest.txt") + latest_txt_items = latest_txt.decoded_content.split() + new_latest_txt_items = latest_txt_items[:2] + config_yml = gh_repo_web.get_contents("_config.yml") + if prerelease: + # If it's a pre-release, we append to current version in latest.txt + new_latest_txt_items.extend([RELEASE_VERSION_BYTES, latest_txt_items[1]]) + # And replace in _config.yml + new_config_yml = re.sub( + b"latest_testing: '[^']*'", + b"latest_testing: '%s'" % RELEASE_VERSION_BYTES, + config_yml.decoded_content, + ) + else: + # New stable release, replace the version + new_latest_txt_items[0] = RELEASE_VERSION_BYTES + # And replace in _config.yml + new_config_yml = re.sub( + b"latest_testing: '[^']*'", + b"latest_testing: ''", + config_yml.decoded_content, + ) + new_config_yml = re.sub( + b"latest_stable: '[^']*'", + b"latest_stable: '%s'" % RELEASE_VERSION_BYTES, + new_config_yml, + ) + # Also update the wiki-settings, these only use x.x notation + new_config_yml = re.sub( + b"wiki_version: '[^']*'", + b"wiki_version: '%s'" % RELEASE_VERSION_BYTES[:3], + new_config_yml, + ) + + # Update the files + print("Updating latest.txt") + gh_repo_web.update_file( + "latest.txt", + "Release %s: latest.txt" % RELEASE_VERSION, + b"\n".join(new_latest_txt_items), + latest_txt.sha, + RELEASE_VERSION, + ) + print("Updating _config.yml") + gh_repo_web.update_file( + "_config.yml", + "Release %s: _config.yml" % RELEASE_VERSION, + new_config_yml, + config_yml.sha, + RELEASE_VERSION, + ) + + # Create pull-request + print("Creating pull request in sabnzbd/sabnzbd.github.io for the update") + gh_repo_web.create_pull( + title=RELEASE_VERSION, + base="master", + body="Automated update of release files", + head=RELEASE_VERSION, + ) + else: + print("Missing GITHUB_TOKEN, cannot push to GitHub!") + else: + print("To push release to GitHub, include 'release' in the name of the tag") + + # Reset! + run_git_command(["reset", "--hard"]) + run_git_command(["clean", "-f"]) diff --git a/builder/requirements.txt b/builder/requirements.txt new file mode 100644 index 0000000..07231fd --- /dev/null +++ b/builder/requirements.txt @@ -0,0 +1,9 @@ +# Basic build requirements +pyinstaller +setuptools +pkginfo +certifi +pygithub + +# For the OSX build specific +dmgbuild; sys_platform == 'darwin' diff --git a/builder/win/NSIS_Installer.nsi b/builder/win/NSIS_Installer.nsi new file mode 100644 index 0000000..d22599f --- /dev/null +++ b/builder/win/NSIS_Installer.nsi @@ -0,0 +1,396 @@ +; -*- coding: utf-8 -*- +; +; Copyright 2008-2015 The SABnzbd-Team +; +; This program is free software; you can redistribute it and/or +; modify it under the terms of the GNU General Public License +; as published by the Free Software Foundation; either version 2 +; of the License, or (at your option) any later version. +; +; This program is distributed in the hope that it will be useful, +; but WITHOUT ANY WARRANTY; without even the implied warranty of +; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +; GNU General Public License for more details. +; +; You should have received a copy of the GNU General Public License +; along with this program; if not, write to the Free Software +; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +Unicode true + +!addplugindir builder\win\nsis\Plugins +!addincludedir builder\win\nsis\Include + +!include "MUI2.nsh" +!include "registerExtension.nsh" +!include "FileFunc.nsh" +!include "LogicLib.nsh" +!include "WinVer.nsh" +!include "nsProcess.nsh" +!include "x64.nsh" +!include "servicelib.nsh" + +;------------------------------------------------------------------ +; +; Marco for removing existing and the current installation +; It shared by the installer and the uninstaller. +; +!define RemovePrev "!insertmacro RemovePrev" +!macro RemovePrev idir + ; Remove the whole dir + ; Users should not be putting stuff here! + RMDir /r "${idir}" +!macroend + +;------------------------------------------------------------------ +; Define names of the product + Name "${SAB_PRODUCT}" + OutFile "${SAB_FILE}" + InstallDir "$PROGRAMFILES\SABnzbd" + + +;------------------------------------------------------------------ +; Some default compiler settings (uncomment and change at will): + SetCompress auto ; (can be off or force) + SetDatablockOptimize on ; (can be off) + CRCCheck on ; (can be off) + AutoCloseWindow false ; (can be true for the window go away automatically at end) + ShowInstDetails hide ; (can be show to have them shown, or nevershow to disable) + SetDateSave off ; (can be on to have files restored to their orginal date) + WindowIcon on + SpaceTexts none + + +;------------------------------------------------------------------ +; Vista/Win7 redirects $SMPROGRAMS to all users without this + RequestExecutionLevel admin + FileErrorText "If you have no admin rights, try to install into a user directory." + + +;------------------------------------------------------------------ +;Variables + Var MUI_TEMP + Var STARTMENU_FOLDER + Var PREV_INST_DIR + +;------------------------------------------------------------------ +;Interface Settings + + !define MUI_ABORTWARNING + + ;Show all languages, despite user's codepage + !define MUI_LANGDLL_ALLLANGUAGES + + !define MUI_ICON "dist\SABnzbd\icons\sabnzbd.ico" + + +;-------------------------------- +;Pages + + !insertmacro MUI_PAGE_LICENSE "dist\SABnzbd\LICENSE.txt" + !define MUI_COMPONENTSPAGE_NODESC + !insertmacro MUI_PAGE_COMPONENTS + + !insertmacro MUI_PAGE_DIRECTORY + + ;Start Menu Folder Page Configuration + !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" + !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\SABnzbd" + !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + !define MUI_STARTMENUPAGE_DEFAULTFOLDER "SABnzbd" + ;Remember the installer language + !define MUI_LANGDLL_REGISTRY_ROOT "HKCU" + !define MUI_LANGDLL_REGISTRY_KEY "Software\SABnzbd" + !define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" + + !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER + + !insertmacro MUI_PAGE_INSTFILES + ; !define MUI_FINISHPAGE_RUN + ; !define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun + ; !define MUI_FINISHPAGE_RUN_TEXT $(MsgRunSAB) + !define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt" + !define MUI_FINISHPAGE_SHOWREADME_TEXT $(MsgShowRelNote) + !define MUI_FINISHPAGE_LINK $(MsgSupportUs) + !define MUI_FINISHPAGE_LINK_LOCATION "https://sabnzbd.org/donate" + + !insertmacro MUI_PAGE_FINISH + + !insertmacro MUI_UNPAGE_CONFIRM + !define MUI_UNPAGE_COMPONENTSPAGE_NODESC + !insertmacro MUI_UNPAGE_COMPONENTS + !insertmacro MUI_UNPAGE_INSTFILES + +;------------------------------------------------------------------ +; Run as user-level at end of install +; DOES NOT WORK +; Function PageFinishRun +; !insertmacro UAC_AsUser_ExecShell "" "$INSTDIR\SABnzbd.exe" "" "" "" +; FunctionEnd + + +;------------------------------------------------------------------ +; Set supported languages +; +; If you edit this list you also need to edit apireg.py in SABnzbd! +; + !insertmacro MUI_LANGUAGE "English" ;first language is the default language + !insertmacro MUI_LANGUAGE "French" + !insertmacro MUI_LANGUAGE "German" + !insertmacro MUI_LANGUAGE "Dutch" + !insertmacro MUI_LANGUAGE "Finnish" + !insertmacro MUI_LANGUAGE "Polish" + !insertmacro MUI_LANGUAGE "Swedish" + !insertmacro MUI_LANGUAGE "Danish" + !insertmacro MUI_LANGUAGE "Norwegian" + !insertmacro MUI_LANGUAGE "Romanian" + !insertmacro MUI_LANGUAGE "Spanish" + !insertmacro MUI_LANGUAGE "PortugueseBR" + !insertmacro MUI_LANGUAGE "Serbian" + !insertmacro MUI_LANGUAGE "Hebrew" + !insertmacro MUI_LANGUAGE "Russian" + !insertmacro MUI_LANGUAGE "Czech" + !insertmacro MUI_LANGUAGE "SimpChinese" + + + +;------------------------------------------------------------------ +;Reserve Files + ;If you are using solid compression, files that are required before + ;the actual installation should be stored first in the data block, + ;because this will make your installer start faster. + + !insertmacro MUI_RESERVEFILE_LANGDLL + + +;------------------------------------------------------------------ +; SECTION main program +; +Section "SABnzbd" SecDummy + + SetOutPath "$INSTDIR" + + ;------------------------------------------------------------------ + ; Make sure old versions are gone (reg-key already read in onInt) + StrCmp $PREV_INST_DIR "" noPrevInstallRemove + ${RemovePrev} "$PREV_INST_DIR" + noPrevInstallRemove: + + ; add files / whatever that need to be installed here. + File /r "dist\SABnzbd\*" + + ;------------------------------------------------------------------ + ; Add firewall rules + liteFirewallW::AddRule "$INSTDIR\SABnzbd.exe" "SABnzbd" + liteFirewallW::AddRule "$INSTDIR\SABnzbd-console.exe" "SABnzbd-console" + + ;------------------------------------------------------------------ + ; Add to registery + WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "" "$INSTDIR" + WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "Installer Language" "$(MsgLangCode)" + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayName" "SABnzbd ${SAB_VERSION}" + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayVersion" '${SAB_VERSION}' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Publisher" 'The SABnzbd Team' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "HelpLink" 'https://forums.sabnzbd.org/' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLInfoAbout" 'https://sabnzbd.org/wiki/' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLUpdateInfo" 'https://sabnzbd.org/' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Comments" 'The automated Usenet download tool' + WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayIcon" '$INSTDIR\icons\sabnzbd.ico' + WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "EstimatedSize" 25674 + WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoRepair" -1 + WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoModify" -1 + ; write out uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + ;Create shortcuts + CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - SafeMode.lnk" "$INSTDIR\SABnzbd.exe" "--server 127.0.0.1:8080 -b1 --no-login" + WriteINIStr "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - Documentation.url" "InternetShortcut" "URL" "https://sabnzbd.org/wiki/" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\Uninstall.exe" + !insertmacro MUI_STARTMENU_WRITE_END +SectionEnd ; end of default section + +Section $(MsgIcon) desktop + CreateShortCut "$DESKTOP\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe" +SectionEnd ; end of desktop icon section + +Section $(MsgAssoc) assoc + ${registerExtension} "$INSTDIR\icons\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File" + ${RefreshShellIcons} +SectionEnd ; end of file association section + +Section /o $(MsgRunAtStart) startup + CreateShortCut "$SMPROGRAMS\Startup\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe" "-b0" +SectionEnd ; + +;------------------------------------------------------------------ +Function .onInit + ; We need to modify the dir here for X64 + ${If} ${RunningX64} + StrCpy $INSTDIR "$PROGRAMFILES64\SABnzbd" + ${Else} + MessageBox MB_OK $(MsgOnly64bit) + ExecShell "open" "https://sabnzbd.org/downloads" + Abort + ${EndIf} + + ;------------------------------------------------------------------ + ; Change settings based on if SAB was already installed + ReadRegStr $PREV_INST_DIR HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "" + StrCmp $PREV_INST_DIR "" noPrevInstall + ; We want to use the user's costom dir if he used one + StrCmp $PREV_INST_DIR "$PROGRAMFILES\SABnzbd" noSpecialDir + StrCmp $PREV_INST_DIR "$PROGRAMFILES64\SABnzbd" noSpecialDir + ; Set what the user had before + StrCpy $INSTDIR "$PREV_INST_DIR" + noSpecialDir: + + ;------------------------------------------------------------------ + ; Check what the user has currently set for install options + IfFileExists "$SMPROGRAMS\Startup\SABnzbd.lnk" 0 endCheckStartup + SectionSetFlags ${startup} 1 + endCheckStartup: + + IfFileExists "$DESKTOP\SABnzbd.lnk" endCheckDesktop 0 + SectionSetFlags ${desktop} 0 ; SAB is installed but desktop-icon not, so uncheck it + endCheckDesktop: + + Push $1 + ReadRegStr $1 HKCR ".nzb" "" ; read current file association + StrCmp "$1" "NZB File" noPrevInstall 0 + SectionSetFlags ${assoc} 0 ; Uncheck it when it wasn't checked before + noPrevInstall: + + ;-------------------------------- + ; Display language chooser + !insertmacro MUI_LANGDLL_DISPLAY + + ;------------------------------------------------------------------ + ; make sure user terminates sabnzbd.exe or else abort + ; + loop: + ${nsProcess::FindProcess} "SABnzbd.exe" $R0 + StrCmp $R0 0 0 endcheck + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgCloseSab) IDOK loop IDCANCEL exitinstall + exitinstall: + ${nsProcess::Unload} + Abort + endcheck: + + ;------------------------------------------------------------------ + ; make sure both services aren't running + ; + !insertmacro SERVICE "running" "SABnzbd" "" + Pop $0 ;response + !insertmacro SERVICE "running" "SABHelper" "" + Pop $1 + ${If} $0 == true + ${OrIf} $1 == true + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgCloseSab) IDOK loop IDCANCEL exitinstall + ; exitinstall already defined above + ${EndIf} + + ;------------------------------------------------------------------ + ; Tell users about the service change + ; + !insertmacro SERVICE "installed" "SABHelper" "" + Pop $0 ;response + ${If} $0 == true + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgServChange) IDOK removeservices IDCANCEL exitinstall + ; exitinstall already defined above + removeservices: + !insertmacro SERVICE "delete" "SABHelper" "" + !insertmacro SERVICE "delete" "SABnzbd" "" + ${EndIf} + +FunctionEnd + +;------------------------------------------------------------------ +; Show the shortcuts at end of install so user can start SABnzbd +; This is instead of us trying to run SAB from the installer +; +Function .onInstSuccess + ExecShell "open" "$SMPROGRAMS\$STARTMENU_FOLDER" +FunctionEnd + +;-------------------------------- +; begin uninstall settings/section +UninstallText $(MsgUninstall) + +Section "un.$(MsgDelProgram)" Uninstall +;make sure sabnzbd.exe isnt running..if so shut it down + ${nsProcess::KillProcess} "SABnzbd.exe" $R0 + ${nsProcess::Unload} + DetailPrint "Process Killed" + + + ; add delete commands to delete whatever files/registry keys/etc you installed here. + Delete "$INSTDIR\uninstall.exe" + DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" + DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" + + ${RemovePrev} "$INSTDIR" + + !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP + + Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd.lnk" + Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" + Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - SafeMode.lnk" + Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - Documentation.url" + RMDir "$SMPROGRAMS\$MUI_TEMP" + + Delete "$SMPROGRAMS\Startup\SABnzbd.lnk" + + Delete "$DESKTOP\SABnzbd.lnk" + + DeleteRegKey HKEY_CURRENT_USER "Software\SABnzbd" + + ${unregisterExtension} ".nzb" "NZB File" + ${RefreshShellIcons} + +SectionEnd ; end of uninstall section + +Section /o "un.$(MsgDelSettings)" DelSettings + DetailPrint "Uninstall settings $LOCALAPPDATA" + Delete "$LOCALAPPDATA\sabnzbd\sabnzbd.ini" + RMDir /r "$LOCALAPPDATA\sabnzbd" +SectionEnd + +; eof + +;-------------------------------- +;Language strings + LangString MsgShowRelNote ${LANG_ENGLISH} "Show Release Notes" + + LangString MsgSupportUs ${LANG_ENGLISH} "Support the project, Donate!" + + LangString MsgCloseSab ${LANG_ENGLISH} "Please close $\"SABnzbd.exe$\" first" + + LangString MsgServChange ${LANG_ENGLISH} "The SABnzbd Windows Service changed in SABnzbd 3.0.0. $\nYou will need to reinstall the SABnzbd service. $\n$\nClick `OK` to remove the existing services or `Cancel` to cancel this upgrade." + + LangString MsgOnly64bit ${LANG_ENGLISH} "The installer only supports 64-bit Windows, use the standalone version to run on 32-bit Windows." + + LangString MsgUninstall ${LANG_ENGLISH} "This will uninstall SABnzbd from your system" + + LangString MsgRunAtStart ${LANG_ENGLISH} "Run at startup" + + LangString MsgIcon ${LANG_ENGLISH} "Desktop Icon" + + LangString MsgAssoc ${LANG_ENGLISH} "NZB File association" + + LangString MsgDelProgram ${LANG_ENGLISH} "Delete Program" + + LangString MsgDelSettings ${LANG_ENGLISH} "Delete Settings" + + LangString MsgRemoveOld ${LANG_ENGLISH} "You cannot overwrite an existing installation. $\n$\nClick `OK` to remove the previous version or `Cancel` to cancel this upgrade." + + LangString MsgRemoveOld2 ${LANG_ENGLISH} "Your settings and data will be preserved." + + LangString MsgLangCode ${LANG_ENGLISH} "en" + +Function un.onInit + !insertmacro MUI_UNGETLANGUAGE +FunctionEnd diff --git a/builder/win/nsis/Include/nsProcess.nsh b/builder/win/nsis/Include/nsProcess.nsh new file mode 100644 index 0000000..9ef6098 --- /dev/null +++ b/builder/win/nsis/Include/nsProcess.nsh @@ -0,0 +1,28 @@ +!define nsProcess::FindProcess `!insertmacro nsProcess::FindProcess` + +!macro nsProcess::FindProcess _FILE _ERR + nsProcess::_FindProcess /NOUNLOAD `${_FILE}` + Pop ${_ERR} +!macroend + + +!define nsProcess::KillProcess `!insertmacro nsProcess::KillProcess` + +!macro nsProcess::KillProcess _FILE _ERR + nsProcess::_KillProcess /NOUNLOAD `${_FILE}` + Pop ${_ERR} +!macroend + +!define nsProcess::CloseProcess `!insertmacro nsProcess::CloseProcess` + +!macro nsProcess::CloseProcess _FILE _ERR + nsProcess::_CloseProcess /NOUNLOAD `${_FILE}` + Pop ${_ERR} +!macroend + + +!define nsProcess::Unload `!insertmacro nsProcess::Unload` + +!macro nsProcess::Unload + nsProcess::_Unload +!macroend diff --git a/builder/win/nsis/Include/registerExtension.nsh b/builder/win/nsis/Include/registerExtension.nsh new file mode 100644 index 0000000..7acffca --- /dev/null +++ b/builder/win/nsis/Include/registerExtension.nsh @@ -0,0 +1,53 @@ +!define registerExtension "!insertmacro registerExtension" +!define unregisterExtension "!insertmacro unregisterExtension" +!define SHCNE_ASSOCCHANGED 0x8000000 +!define SHCNF_IDLIST 0 + +; Source = http://nsis.sourceforge.net/File_Association +; Patched for SABnzbd by swi-tch + +!macro registerExtension icon executable extension description + Push "${icon}" ; "full path to icon.ico" + Push "${executable}" ; "full path to my.exe" + Push "${extension}" ; ".mkv" + Push "${description}" ; "MKV File" + Call registerExtension +!macroend + +; back up old value of .opt +Function registerExtension +!define Index "Line${__LINE__}" + pop $R0 ; ext name + pop $R1 + pop $R2 + pop $R3 + push $1 + push $0 + DeleteRegKey HKEY_CURRENT_USER "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$R1" + WriteRegStr HKCR $R1 "" $R0 + WriteRegStr HKCR $R0 "" $R0 + WriteRegStr HKCR "$R0\shell" "" "open" + WriteRegStr HKCR "$R0\DefaultIcon" "" "$R3,0" + WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1"' + WriteRegStr HKCR "$R0\shell\edit" "" "Edit $R0" + WriteRegStr HKCR "$R0\shell\edit\command" "" '"$R2" "%1"' + pop $0 + pop $1 +!undef Index +System::Call 'Shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i ${SHCNF_IDLIST}, i 0, i 0)' +FunctionEnd + +!macro unregisterExtension extension description + Push "${extension}" ; ".mkv" + Push "${description}" ; "MKV File" + Call un.unregisterExtension +!macroend + +Function un.unregisterExtension + pop $R1 ; description + pop $R0 ; extension +!define Index "Line${__LINE__}" + DeleteRegKey HKCR $R0 +!undef Index +System::Call 'Shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i ${SHCNF_IDLIST}, i 0, i 0)' +FunctionEnd \ No newline at end of file diff --git a/builder/win/nsis/Include/servicelib.nsh b/builder/win/nsis/Include/servicelib.nsh new file mode 100644 index 0000000..aa2da83 --- /dev/null +++ b/builder/win/nsis/Include/servicelib.nsh @@ -0,0 +1,411 @@ +; NSIS SERVICE LIBRARY - servicelib.nsh +; Version 1.8.1 - Jun 21th, 2013 +; Questions/Comments - dselkirk@hotmail.com +; +; Description: +; Provides an interface to window services +; +; Inputs: +; action - systemlib action ie. create, delete, start, stop, pause, +; continue, installed, running, status +; name - name of service to manipulate +; param - action parameters; usage: var1=value1;var2=value2;...etc. +; (don't forget to add a ';' after the last value!) +; +; Actions: +; create - creates a new windows service +; Parameters: +; path - path to service executable +; autostart - automatically start with system ie. 1|0 +; interact - interact with the desktop ie. 1|0 +; depend - service dependencies +; user - user that runs the service +; password - password of the above user +; display - display name in service's console +; description - Description of service +; starttype - start type (supersedes autostart) +; servicetype - service type (supersedes interact) +; +; delete - deletes a windows service +; start - start a stopped windows service +; stop - stops a running windows service +; pause - pauses a running windows service +; continue - continues a paused windows service +; installed - is the provided service installed +; Parameters: +; action - if true then invokes the specified action +; running - is the provided service running +; Parameters: +; action - if true then invokes the specified action +; status - check the status of the provided service +; +; Usage: +; Method 1: +; Push "action" +; Push "name" +; Push "param" +; Call Service +; Pop $0 ;response +; +; Method 2: +; !insertmacro SERVICE "action" "name" "param" +; +; History: +; 1.0 - 09/15/2003 - Initial release +; 1.1 - 09/16/2003 - Changed &l to i, thx brainsucker +; 1.2 - 02/29/2004 - Fixed documentation. +; 1.3 - 01/05/2006 - Fixed interactive flag and pop order (Kichik) +; 1.4 - 12/07/2006 - Added display and depend, fixed datatypes (Vitoco) +; 1.5 - 06/25/2008 - Added description of service.(DeSafe.com/liuqixing#gmail.com) +; 1.5.1 - 06/12/2009 - Added use of __UNINSTALL__ +; 1.6 - 08/02/2010 - Fixed description implementation (Anders) +; 1.7 - 04/11/2010 - Added get running service process id (Nico) +; 1.8 - 24/03/2011 - Added starttype and servicetype (Sergius) +; 1.8.1 - 21/06/2013 - Added dynamic ASCII & Unicode support (Zinthose) + +!ifndef SERVICELIB + !define SERVICELIB + + !define SC_MANAGER_ALL_ACCESS 0x3F + !define SC_STATUS_PROCESS_INFO 0x0 + !define SERVICE_ALL_ACCESS 0xF01FF + + !define SERVICE_CONTROL_STOP 1 + !define SERVICE_CONTROL_PAUSE 2 + !define SERVICE_CONTROL_CONTINUE 3 + + !define SERVICE_STOPPED 0x1 + !define SERVICE_START_PENDING 0x2 + !define SERVICE_STOP_PENDING 0x3 + !define SERVICE_RUNNING 0x4 + !define SERVICE_CONTINUE_PENDING 0x5 + !define SERVICE_PAUSE_PENDING 0x6 + !define SERVICE_PAUSED 0x7 + + !define SERVICE_KERNEL_DRIVER 0x00000001 + !define SERVICE_FILE_SYSTEM_DRIVER 0x00000002 + !define SERVICE_WIN32_OWN_PROCESS 0x00000010 + !define SERVICE_WIN32_SHARE_PROCESS 0x00000020 + !define SERVICE_INTERACTIVE_PROCESS 0x00000100 + + + !define SERVICE_BOOT_START 0x00000000 + !define SERVICE_SYSTEM_START 0x00000001 + !define SERVICE_AUTO_START 0x00000002 + !define SERVICE_DEMAND_START 0x00000003 + !define SERVICE_DISABLED 0x00000004 + + ## Added by Zinthose for Native Unicode Support + !ifdef NSIS_UNICODE + !define APITAG "W" + !else + !define APITAG "A" + !endif + + !macro SERVICE ACTION NAME PARAM + Push '${ACTION}' + Push '${NAME}' + Push '${PARAM}' + !ifdef __UNINSTALL__ + Call un.Service + !else + Call Service + !endif + !macroend + + !macro FUNC_GETPARAM + Push $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + Push $6 + Push $7 + Exch 8 + Pop $1 ;name + Exch 8 + Pop $2 ;source + StrCpy $0 "" + StrLen $7 $2 + StrCpy $3 0 + lbl_loop: + IntCmp $3 $7 0 0 lbl_done + StrLen $4 "$1=" + StrCpy $5 $2 $4 $3 + StrCmp $5 "$1=" 0 lbl_next + IntOp $5 $3 + $4 + StrCpy $3 $5 + lbl_loop2: + IntCmp $3 $7 0 0 lbl_done + StrCpy $6 $2 1 $3 + StrCmp $6 ";" 0 lbl_next2 + IntOp $6 $3 - $5 + StrCpy $0 $2 $6 $5 + Goto lbl_done + lbl_next2: + IntOp $3 $3 + 1 + Goto lbl_loop2 + lbl_next: + IntOp $3 $3 + 1 + Goto lbl_loop + lbl_done: + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Exch 2 + Pop $6 + Pop $7 + Exch $0 + !macroend + + !macro CALL_GETPARAM VAR NAME DEFAULT LABEL + Push $1 + Push ${NAME} + Call ${UN}GETPARAM + Pop $6 + StrCpy ${VAR} "${DEFAULT}" + StrCmp $6 "" "${LABEL}" 0 + StrCpy ${VAR} $6 + !macroend + + !macro FUNC_SERVICE UN + Push $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + Push $6 + Push $7 + Exch 8 + Pop $1 ;param + Exch 8 + Pop $2 ;name + Exch 8 + Pop $3 ;action + ;$0 return + ;$4 OpenSCManager + ;$5 OpenService + + StrCpy $0 "false" + System::Call 'advapi32::OpenSCManager${APITAG}(n, n, i ${SC_MANAGER_ALL_ACCESS}) i.r4' + IntCmp $4 0 lbl_done + StrCmp $3 "create" lbl_create + System::Call 'advapi32::OpenService${APITAG}(i r4, t r2, i ${SERVICE_ALL_ACCESS}) i.r5' + IntCmp $5 0 lbl_done + + lbl_select: + StrCmp $3 "delete" lbl_delete + StrCmp $3 "start" lbl_start + StrCmp $3 "stop" lbl_stop + StrCmp $3 "pause" lbl_pause + StrCmp $3 "continue" lbl_continue + StrCmp $3 "installed" lbl_installed + StrCmp $3 "running" lbl_running + StrCmp $3 "status" lbl_status + StrCmp $3 "processid" lbl_processid + Goto lbl_done + + ; create service + lbl_create: + Push $R1 ;depend + Push $R2 ;user + Push $R3 ;password + Push $R4 ;servicetype/interact + Push $R5 ;starttype/autostart + Push $R6 ;path + Push $R7 ;display + Push $R8 ;description + + !insertmacro CALL_GETPARAM $R1 "depend" "n" "lbl_depend" + StrCpy $R1 't "$R1"' + lbl_depend: + StrCmp $R1 "n" 0 lbl_machine ;old name of depend param + !insertmacro CALL_GETPARAM $R1 "machine" "n" "lbl_machine" + StrCpy $R1 't "$R1"' + lbl_machine: + + !insertmacro CALL_GETPARAM $R2 "user" "n" "lbl_user" + StrCpy $R2 't "$R2"' + lbl_user: + + !insertmacro CALL_GETPARAM $R3 "password" "n" "lbl_password" + StrCpy $R3 't "$R3"' + lbl_password: + + !insertmacro CALL_GETPARAM $R4 "interact" "${SERVICE_WIN32_OWN_PROCESS}" "lbl_interact" + StrCpy $6 ${SERVICE_WIN32_OWN_PROCESS} + IntCmp $R4 0 +2 + IntOp $6 $6 | ${SERVICE_INTERACTIVE_PROCESS} + StrCpy $R4 $6 + lbl_interact: + + !insertmacro CALL_GETPARAM $R4 "servicetype" "$R4" "lbl_servicetype" + lbl_servicetype: + + !insertmacro CALL_GETPARAM $R5 "autostart" "${SERVICE_DEMAND_START}" "lbl_autostart" + StrCpy $6 ${SERVICE_DEMAND_START} + IntCmp $R5 0 +2 + StrCpy $6 ${SERVICE_AUTO_START} + StrCpy $R5 $6 + lbl_autostart: + + !insertmacro CALL_GETPARAM $R5 "starttype" "$R5" "lbl_starttype" + lbl_starttype: + + !insertmacro CALL_GETPARAM $R6 "path" "n" "lbl_path" + lbl_path: + + !insertmacro CALL_GETPARAM $R7 "display" "$2" "lbl_display" + lbl_display: + + !insertmacro CALL_GETPARAM $R8 "description" "$2" "lbl_description" + lbl_description: + + System::Call 'advapi32::CreateService${APITAG}(i r4, t r2, t R7, i ${SERVICE_ALL_ACCESS}, \ + i R4, i R5, i 0, t R6, n, n, $R1, $R2, $R3) i.r6' + + ; write description of service (SERVICE_CONFIG_DESCRIPTION) + System::Call 'advapi32::ChangeServiceConfig2${APITAG}(ir6,i1,*t "$R8")i.R7' + strcmp $R7 "error" 0 lbl_descriptioncomplete + WriteRegStr HKLM "SYSTEM\CurrentControlSet\Services\$2" "Description" $R8 + lbl_descriptioncomplete: + + Pop $R8 + Pop $R7 + Pop $R6 + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Pop $R1 + StrCmp $6 0 lbl_done lbl_good + + ; delete service + lbl_delete: + System::Call 'advapi32::DeleteService(i r5) i.r6' + StrCmp $6 0 lbl_done lbl_good + + ; start service + lbl_start: + System::Call 'advapi32::StartService${APITAG}(i r5, i 0, i 0) i.r6' + StrCmp $6 0 lbl_done lbl_good + + ; stop service + lbl_stop: + Push $R1 + System::Call '*(i,i,i,i,i,i,i) i.R1' + System::Call 'advapi32::ControlService(i r5, i ${SERVICE_CONTROL_STOP}, i $R1) i' + System::Free $R1 + Pop $R1 + StrCmp $6 0 lbl_done lbl_good + + ; pause service + lbl_pause: + Push $R1 + System::Call '*(i,i,i,i,i,i,i) i.R1' + System::Call 'advapi32::ControlService(i r5, i ${SERVICE_CONTROL_PAUSE}, i $R1) i' + System::Free $R1 + Pop $R1 + StrCmp $6 0 lbl_done lbl_good + + ; continue service + lbl_continue: + Push $R1 + System::Call '*(i,i,i,i,i,i,i) i.R1' + System::Call 'advapi32::ControlService(i r5, i ${SERVICE_CONTROL_CONTINUE}, i $R1) i' + System::Free $R1 + Pop $R1 + StrCmp $6 0 lbl_done lbl_good + + ; is installed + lbl_installed: + !insertmacro CALL_GETPARAM $7 "action" "" "lbl_good" + StrCpy $3 $7 + Goto lbl_select + + ; is service running + lbl_running: + Push $R1 + System::Call '*(i,i,i,i,i,i,i) i.R1' + System::Call 'advapi32::QueryServiceStatus(i r5, i $R1) i' + System::Call '*$R1(i, i.r6)' + System::Free $R1 + Pop $R1 + IntFmt $6 "0x%X" $6 + StrCmp $6 ${SERVICE_RUNNING} 0 lbl_done + !insertmacro CALL_GETPARAM $7 "action" "" "lbl_good" + StrCpy $3 $7 + Goto lbl_select + + lbl_status: + Push $R1 + System::Call '*(i,i,i,i,i,i,i) i.R1' + System::Call 'advapi32::QueryServiceStatus(i r5, i $R1) i' + System::Call '*$R1(i, i .r6)' + System::Free $R1 + Pop $R1 + IntFmt $6 "0x%X" $6 + StrCpy $0 "running" + IntCmp $6 ${SERVICE_RUNNING} lbl_done + StrCpy $0 "stopped" + IntCmp $6 ${SERVICE_STOPPED} lbl_done + StrCpy $0 "start_pending" + IntCmp $6 ${SERVICE_START_PENDING} lbl_done + StrCpy $0 "stop_pending" + IntCmp $6 ${SERVICE_STOP_PENDING} lbl_done + StrCpy $0 "running" + IntCmp $6 ${SERVICE_RUNNING} lbl_done + StrCpy $0 "continue_pending" + IntCmp $6 ${SERVICE_CONTINUE_PENDING} lbl_done + StrCpy $0 "pause_pending" + IntCmp $6 ${SERVICE_PAUSE_PENDING} lbl_done + StrCpy $0 "paused" + IntCmp $6 ${SERVICE_PAUSED} lbl_done + StrCpy $0 "unknown" + Goto lbl_done + + lbl_processid: + Push $R1 + Push $R2 + System::Call '*(i,i,i,i,i,i,i,i,i) i.R1' + System::Call '*(i 0) i.R2' + System::Call "advapi32::QueryServiceStatusEx(i r5, i ${SC_STATUS_PROCESS_INFO}, i $R1, i 36, i $R2) i" + System::Call "*$R1(i,i,i,i,i,i,i, i .r0)" + System::Free $R2 + System::Free $R1 + Pop $R2 + Pop $R1 + Goto lbl_done + + lbl_good: + StrCpy $0 "true" + lbl_done: + IntCmp $5 0 +2 + System::Call 'advapi32::CloseServiceHandle(i r5) n' + IntCmp $4 0 +2 + System::Call 'advapi32::CloseServiceHandle(i r4) n' + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Exch 3 + Pop $5 + Pop $7 + Pop $6 + Exch $0 + !macroend + + Function Service + !insertmacro FUNC_SERVICE "" + FunctionEnd + + Function GetParam + !insertmacro FUNC_GETPARAM + FunctionEnd + + !undef APITAG +!endif \ No newline at end of file diff --git a/builder/win/nsis/Plugins/liteFirewallW.dll b/builder/win/nsis/Plugins/liteFirewallW.dll new file mode 100644 index 0000000..a2e51b1 Binary files /dev/null and b/builder/win/nsis/Plugins/liteFirewallW.dll differ diff --git a/builder/win/nsis/Plugins/nsProcess.dll b/builder/win/nsis/Plugins/nsProcess.dll new file mode 100644 index 0000000..2478624 Binary files /dev/null and b/builder/win/nsis/Plugins/nsProcess.dll differ