diff --git a/.github/workflows/installer-macos.yml b/.github/workflows/installer-macos.yml index bf1b6dce3ea..f30a4267b1e 100644 --- a/.github/workflows/installer-macos.yml +++ b/.github/workflows/installer-macos.yml @@ -10,7 +10,9 @@ on: - '**.sh' - '!**.md' - '!installers/Windows/**' + - '!installers-conda/**' - '!.github/workflows/installer-win.yml' + - '!.github/workflows/installers-conda.yml' release: types: diff --git a/.github/workflows/installer-win.yml b/.github/workflows/installer-win.yml index d41f17b0906..fc622d05c94 100644 --- a/.github/workflows/installer-win.yml +++ b/.github/workflows/installer-win.yml @@ -11,7 +11,9 @@ on: - '**.sh' - '!**.md' - '!installers/macOS/**' + - '!installers-conda/**' - '!.github/workflows/installer-macos.yml' + - '!.github/workflows/installers-conda.yml' release: types: diff --git a/.github/workflows/installers-conda.yml b/.github/workflows/installers-conda.yml new file mode 100644 index 00000000000..3cbb3cc61d8 --- /dev/null +++ b/.github/workflows/installers-conda.yml @@ -0,0 +1,217 @@ +on: + pull_request: + paths: + - 'installers-conda/**' + - '.github/workflows/installers-conda.yml' + - 'requirements/*.yml' + - 'MANIFEST.in' + - '**.bat' + - '**.py' + - '**.sh' + - '!**.md' + - '!installers/**' + - '!.github/workflows/installer-win.yml' + - '!.github/workflows/installer-macos.yml' + + release: + types: + - created + +name: Create conda-based installers for Windows, macOS, and Linux + +jobs: + build-noarch-conda-pkgs: + name: Build ${{ matrix.pkg }} + runs-on: ubuntu-latest + if: github.event_name != 'release' + strategy: + matrix: + pkg: ["python-lsp-server", "qdarkstyle", "qtconsole"] + python-version: ["3.9"] + defaults: + run: + shell: bash -l {0} + working-directory: ${{ github.workspace }}/installers-conda + env: + DISTDIR: ${{ github.workspace }}/installers-conda/dist + pkg: ${{ matrix.pkg }} + artifact_name: ${{ matrix.pkg }}_${{ matrix.python-version }} + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Build Environment + uses: mamba-org/provision-with-micromamba@main + with: + environment-file: installers-conda/build-environment.yml + extra-specs: python=${{ matrix.python-version }} + + - name: Build Conda Packages + run: python build_conda_pkgs.py --build $pkg + + - name: Build Artifact + run: tar -a -C $CONDA_PREFIX -cf $PWD/${artifact_name}.tar.bz2 conda-bld + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + path: ${{ github.workspace }}/installers-conda/${{ env.artifact_name }}.tar.bz2 + name: ${{ env.artifact_name }} + + build-matrix: + name: Determine Build Matrix + runs-on: ubuntu-latest + outputs: + target_platform: ${{ steps.build-matrix.outputs.target_platform }} + include: ${{ steps.build-matrix.outputs.include }} + python_version: ${{ steps.build-matrix.outputs.python_version }} + steps: + - id: build-matrix + run: | + target_platform="'osx-64', 'osx-arm64', 'linux-64'" + include="\ + {'os': 'macos-11', 'target-platform': 'osx-64'},\ + {'os': 'macos-latest', 'target-platform': 'osx-arm64'},\ + {'os': 'ubuntu-latest', 'target-platform': 'linux-64'}\ + " + python_version="'3.9'" + + if [[ ${GITHUB_EVENT_NAME} == 'release' ]]; then + target_platform=$target_platform", 'win-64'" + include=$include",{'os': 'windows-latest', 'target-platform': 'win-64'}" + fi + + echo "target_platform=[$target_platform]" >> $GITHUB_OUTPUT + echo "include=[$include]" >> $GITHUB_OUTPUT + echo "python_version=[$python_version]" >> $GITHUB_OUTPUT + + build-installers: + name: Build installer for ${{ matrix.target-platform }} Python-${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + needs: [build-matrix, build-noarch-conda-pkgs] + if: always() + strategy: + matrix: + target-platform: ${{fromJson(needs.build-matrix.outputs.target_platform)}} + python-version: ${{fromJson(needs.build-matrix.outputs.python_version)}} + include: ${{fromJson(needs.build-matrix.outputs.include)}} + + defaults: + run: + shell: bash -l {0} + working-directory: ${{ github.workspace }}/installers-conda + env: + DISTDIR: ${{ github.workspace }}/installers-conda/dist + MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_INSTALLER_CERTIFICATE: ${{ secrets.MACOS_INSTALLER_CERTIFICATE }} + APPLICATION_PWD: ${{ secrets.APPLICATION_PWD }} + CONSTRUCTOR_TARGET_PLATFORM: ${{ matrix.target-platform }} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Build Environment + uses: mamba-org/provision-with-micromamba@main + with: + environment-file: installers-conda/build-environment.yml + extra-specs: python=${{ matrix.python-version }} + + - name: Download Local Conda Packages + if: github.event_name != 'release' + uses: actions/download-artifact@v3 + with: + path: ${{ github.workspace }}/installers-conda/artifacts + + - name: Create Local Conda Channel + run: | + CONDA_BLD_PATH=$HOME/conda-bld + echo "CONDA_BLD_PATH=$CONDA_BLD_PATH" >> $GITHUB_ENV + + files=($(find $PWD/artifacts -name *.tar.bz2)) + echo ${files[@]} + cd $(dirname $CONDA_BLD_PATH) + for file in ${files[@]}; do + tar -xf $file + done + + mamba index $CONDA_BLD_PATH + + mamba search -c $CONDA_BLD_PATH --override-channels + + - name: Build ${{ matrix.target-platform }} conda packages + run: | + pkgs=("spyder") + if [[ $GITHUB_EVENT_NAME != "release" ]]; then + pkgs+=("spyder-kernels") + fi + python build_conda_pkgs.py --build ${pkgs[@]} + + - name: Create Keychain + if: github.event_name == 'release' && runner.os == 'macOS' + run: | + ./certkeychain.sh "${MACOS_CERTIFICATE_PWD}" "${MACOS_CERTIFICATE}" "${MACOS_INSTALLER_CERTIFICATE}" + CNAME=$(security find-identity -p codesigning -v | pcregrep -o1 "\(([0-9A-Z]+)\)") + echo "CNAME=$CNAME" >> $GITHUB_ENV + + _codesign=$(which codesign) + if [[ $_codesign =~ ${CONDA_PREFIX}.* ]]; then + # Find correct codesign + echo "Moving $_codesign..." + mv $_codesign ${_codesign}.bak + fi + + - name: Build Package Installer + run: | + [[ -n $CNAME ]] && args=("--cert-id" "$CNAME") || args=() + python build_installers.py ${args[@]} + PKG_FILE=$(python build_installers.py --artifact-name) + PKG_NAME=$(basename $PKG_FILE) + echo "PKG_FILE=$PKG_FILE" >> $GITHUB_ENV + echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV + + - name: Test Application Bundle + if: runner.os == 'macOS' + run: | + installer -dumplog -pkg $PKG_FILE -target CurrentUserHomeDirectory 2>&1 + app_path=$HOME/Applications/Spyder.app + if [[ -e "$app_path" ]]; then + ls -al $app_path/Contents/MacOS + cat $app_path/Contents/Info.plist + echo "" + else + echo "$app_path does not exist" + fi + + - name: Notarize package installer + if: github.event_name == 'release' && runner.os == 'macOS' + run: ./notarize.sh -p $APPLICATION_PWD $PKG_FILE + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + path: ${{ env.PKG_FILE }} + name: ${{ env.PKG_NAME }} + + - name: Get Release + if: github.event_name == 'release' + id: get_release + env: + GITHUB_TOKEN: ${{ github.token }} + uses: bruceadams/get-release@v1.2.0 + + - name: Upload Release Asset + if: github.event_name == 'release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.get_release.outputs.upload_url }} + asset_path: ${{ env.PKG_FILE }} + asset_name: ${{ env.PKG_NAME }} + asset_content_type: application/octet-stream diff --git a/.github/workflows/test-files.yml b/.github/workflows/test-files.yml index d61b393e5f8..25b3fc6d267 100644 --- a/.github/workflows/test-files.yml +++ b/.github/workflows/test-files.yml @@ -14,7 +14,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' pull_request: @@ -30,7 +30,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' jobs: diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 2d1dee2fcd6..8eea69643f3 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -14,7 +14,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' pull_request: @@ -30,7 +30,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' jobs: diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index a2bcb7d5c11..c547934cb14 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -14,7 +14,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' pull_request: @@ -30,7 +30,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' jobs: diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml index aebe9019a5a..80f47cad82e 100644 --- a/.github/workflows/test-win.yml +++ b/.github/workflows/test-win.yml @@ -14,7 +14,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' pull_request: @@ -30,7 +30,7 @@ on: - '**.bat' - '**.py' - '**.sh' - - '!installers/**' + - '!installers*/**' - '!.github/workflows/installer*.yml' jobs: diff --git a/installers-conda/build-environment.yml b/installers-conda/build-environment.yml new file mode 100644 index 00000000000..c90c95b8637 --- /dev/null +++ b/installers-conda/build-environment.yml @@ -0,0 +1,11 @@ +name: spy-inst +channels: + - napari/label/bundle_tools + - conda-forge +dependencies: + - boa + - conda-standalone + - constructor + - gitpython + - ruamel.yaml.jinja2 + - setuptools_scm diff --git a/installers-conda/build_conda_pkgs.py b/installers-conda/build_conda_pkgs.py new file mode 100644 index 00000000000..42918205b1f --- /dev/null +++ b/installers-conda/build_conda_pkgs.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Build conda packages to local channel. + +This module builds conda packages for Spyder and external-deps for +inclusion in the conda-based installer. The Following classes are +provided for each package: + SpyderCondaPkg + PylspCondaPkg + QdarkstyleCondaPkg + QtconsoleCondaPkg + SpyderKernelsCondaPkg + +Spyder will be packaged from this repository (in its checked-out state). +qdarkstyle, qtconsole, and spyder-kernels will be packaged from the +external-deps directory of this repository (in its checked-out state). +Python-lsp-server, however, will be packaged from the upstream remote +at the same commit as the external-deps state. + +Alternatively, any external-deps may be packaged from a local git repository +(in its checked out state) by setting the appropriate environment variable +from the following: + PYTHON_LSP_SERVER_SOURCE + QDARKSTYLE_SOURCE + QTCONSOLE_SOURCE + SPYDER_KERNELS_SOURCE +""" + +# Standard library imports +import os +import re +from argparse import ArgumentParser +from configparser import ConfigParser +from datetime import timedelta +from logging import Formatter, StreamHandler, getLogger +from pathlib import Path +from shutil import rmtree +from subprocess import check_call +from textwrap import dedent +from time import time + +# Third-party imports +from git import Repo +from ruamel.yaml import YAML +from setuptools_scm import get_version + +fmt = Formatter('%(asctime)s [%(levelname)s] [%(name)s] -> %(message)s') +h = StreamHandler() +h.setFormatter(fmt) +logger = getLogger('BuildCondaPkgs') +logger.addHandler(h) +logger.setLevel('INFO') + +HERE = Path(__file__).parent +DIST = HERE / "dist" +RESOURCES = HERE / "resources" +EXTDEPS = HERE.parent / "external-deps" +SPECS = DIST / "specs.yaml" + +DIST.mkdir(exist_ok=True) + + +def remove_readonly(func, path, exc): + """ + Change readonly status of file. + Windows file systems may require this if rmdir fails + """ + import errno, stat + excvalue = exc[1] + if func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES: + os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777 + func(path) + else: + raise + + +class BuildCondaPkg: + name = None + src_path = None + feedstock = None + shallow_ver = None + + _yaml = YAML(typ='jinja2') + _yaml.indent(mapping=2, sequence=4, offset=2) + + def __init__(self, data={}, debug=False): + # ---- Setup logger + self.logger = getLogger(self.__class__.__name__) + if not self.logger.handlers: + self.logger.addHandler(h) + self.logger.setLevel('INFO') + + self.debug = debug + + self._get_source() + self._get_version() + + self.data = {'version': self.version} + self.data.update(data) + + self.fdstk_path = HERE / self.feedstock.split("/")[-1] + + self.yaml = None + + self._patched_meta = False + self._patched_build = False + + def _get_source(self): + self._build_cleanup() + + if not self.src_path.exists(): + cfg = ConfigParser() + cfg.read(EXTDEPS / self.name / '.gitrepo') + # Clone from remote + repo = Repo.clone_from( + cfg['subrepo']['remote'], + to_path=self.src_path, shallow_exclude=self.shallow_ver + ) + repo.git.checkout(cfg['subrepo']['commit']) + + def _build_cleanup(self): + if self.src_path.exists() and self.src_path == HERE / self.name: + logger.info(f"Removing {self.src_path}...") + rmtree(self.src_path, onerror=remove_readonly) + + def _get_version(self): + self.version = get_version(self.src_path).split('+')[0] + + def _clone_feedstock(self): + if self.fdstk_path.exists(): + self.logger.info(f"Removing existing {self.fdstk_path}...") + rmtree(self.fdstk_path, onerror=remove_readonly) + + self.logger.info(f"Cloning feedstock to {self.fdstk_path}...") + check_call(["git", "clone", str(self.feedstock), str(self.fdstk_path)]) + + def _patch_meta(self): + pass + + def patch_meta(self): + if self._patched_meta: + return + + self.logger.info("Patching 'meta.yaml'...") + + file = self.fdstk_path / "recipe" / "meta.yaml" + text = file.read_text() + for k, v in self.data.items(): + text = re.sub(f".*set {k} =.*", f'{{% set {k} = "{v}" %}}', text) + + self.yaml = self._yaml.load(text) + + self.yaml['source'] = {'path': str(self.src_path)} + + self.yaml.pop('test', None) + if 'outputs' in self.yaml: + for out in self.yaml['outputs']: + out.pop('test', None) + + self._patch_meta() + + self._yaml.dump_all([self.yaml], file) + + self._patched_meta = True + + def _patch_build(self): + pass + + def patch_build(self): + if self._patched_build: + return + + self.logger.info("Patching build script...") + self._patch_build() + self._patched_build = True + + def build(self): + t0 = time() + try: + # self._git_init_src_path() + self._clone_feedstock() + self.patch_meta() + self.patch_build() + + self.logger.info("Building conda package " + f"{self.name}={self.version}...") + check_call( + ["mamba", "mambabuild", str(self.fdstk_path / "recipe")] + ) + finally: + self._patched_meta = False + self._patched_build = False + if not self.debug: + self.logger.info(f"Removing {self.fdstk_path}...") + rmtree(self.fdstk_path, onerror=remove_readonly) + + self._build_cleanup() + + elapse = timedelta(seconds=int(time() - t0)) + self.logger.info(f"Build time = {elapse}") + + +class SpyderCondaPkg(BuildCondaPkg): + name = "spyder" + src_path = HERE.parent + feedstock = "https://github.com/conda-forge/spyder-feedstock" + shallow_ver = "v5.3.2" + + def _patch_meta(self): + self.yaml['build'].pop('osx_is_app', None) + self.yaml.pop('app', None) + + patches = self.yaml['source'].get('patches', []) + patches.append(str(RESOURCES / "installers-conda.patch")) + self.yaml['source']['patches'] = patches + + def _patch_build(self): + if os.name == 'posix': + file = self.fdstk_path / "recipe" / "build.sh" + build_patch = RESOURCES / "build-patch.sh" + text = file.read_text() + text += build_patch.read_text() + file.write_text(text) + if os.name == 'nt': + file = self.fdstk_path / "recipe" / "bld.bat" + text = file.read_text() + text = text.replace( + r"copy %RECIPE_DIR%\menu-windows.json %MENU_DIR%\spyder_shortcut.json", + """powershell -Command""" + r""" "(gc %SRC_DIR%\installers-conda\resources\spyder-menu.json)""" + r""" -replace '__PKG_VERSION__', '%PKG_VERSION%' | """ + r"""Out-File -encoding ASCII %MENU_DIR%\spyder-menu.json" """ + ) + file.write_text(text) + +class PylspCondaPkg(BuildCondaPkg): + name = "python-lsp-server" + src_path = Path( + os.environ.get('PYTHON_LSP_SERVER_SOURCE', HERE / name) + ) + feedstock = "https://github.com/conda-forge/python-lsp-server-feedstock" + shallow_ver = "v1.4.1" + + +class QdarkstyleCondaPkg(BuildCondaPkg): + name = "qdarkstyle" + src_path = Path( + os.environ.get('QDARKSTYLE_SOURCE', HERE / name) + ) + feedstock = "https://github.com/conda-forge/qdarkstyle-feedstock" + shallow_ver = "v3.0.2" + + +class QtconsoleCondaPkg(BuildCondaPkg): + name = "qtconsole" + src_path = Path( + os.environ.get('QTCONSOLE_SOURCE', HERE / name) + ) + feedstock = "https://github.com/conda-forge/qtconsole-feedstock" + shallow_ver = "5.3.1" + + def _patch_meta(self): + for out in self.yaml['outputs']: + out.pop("test", None) + + +class SpyderKernelsCondaPkg(BuildCondaPkg): + name = "spyder-kernels" + src_path = Path( + os.environ.get('SPYDER_KERNELS_SOURCE', HERE / name) + ) + feedstock = "https://github.com/conda-forge/spyder-kernels-feedstock" + shallow_ver = "v2.3.1" + + +PKGS = { + SpyderCondaPkg.name: SpyderCondaPkg, + PylspCondaPkg.name: PylspCondaPkg, + QdarkstyleCondaPkg.name: QdarkstyleCondaPkg, + QtconsoleCondaPkg.name: QtconsoleCondaPkg, + SpyderKernelsCondaPkg.name: SpyderKernelsCondaPkg +} + +if __name__ == "__main__": + p = ArgumentParser( + description=dedent( + """ + Build conda packages from local Spyder and external-deps sources. + Alternative git repo for python-lsp-server may be provided by + setting the environment variable PYTHON_LSP_SERVER_SOURCE, + otherwise the upstream remote will be used. All other external-deps + use the subrepo source within the Spyder repo. + """ + ), + usage="python build_conda_pkgs.py " + "[--build BUILD [BUILD] ...] [--debug]", + ) + p.add_argument( + '--debug', action='store_true', default=False, + help="Do not remove cloned feedstocks" + ) + p.add_argument( + '--build', nargs="+", default=PKGS.keys(), + help=("Space-separated list of packages to build. " + f"Default is {list(PKGS.keys())}") + ) + args = p.parse_args() + + logger.info(f"Building local conda packages {list(args.build)}...") + t0 = time() + + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + + for k in args.build: + if SPECS.exists(): + specs = yaml.load(SPECS.read_text()) + else: + specs = {k: "" for k in PKGS} + + pkg = PKGS[k](debug=args.debug) + pkg.build() + specs[k] = "=" + pkg.version + + yaml.dump(specs, SPECS) + + elapse = timedelta(seconds=int(time() - t0)) + logger.info(f"Total build time = {elapse}") diff --git a/installers-conda/build_installers.py b/installers-conda/build_installers.py new file mode 100644 index 00000000000..869a1f19630 --- /dev/null +++ b/installers-conda/build_installers.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Create Spyder installers using `constructor`. + +It creates a `construct.yaml` file with the needed settings +and then runs `constructor`. + +Some environment variables we use: + +CONSTRUCTOR_TARGET_PLATFORM: + conda-style platform (as in `platform` in `conda info -a` output) +CONSTRUCTOR_CONDA_EXE: + when the target platform is not the same as the host, constructor + needs a path to a conda-standalone (or micromamba) executable for + that platform. needs to be provided in this env var in that case! +CONSTRUCTOR_SIGNING_CERTIFICATE: + Path to PFX certificate to sign the EXE installer on Windows +""" + +# Standard library imports +from argparse import ArgumentParser +from datetime import timedelta +from distutils.spawn import find_executable +from functools import partial +import json +from logging import getLogger +import os +from pathlib import Path +import platform +import re +from subprocess import check_call +import sys +from textwrap import dedent, indent +from time import time +import zipfile + +# Third-party imports +from ruamel.yaml import YAML + +# Local imports +from build_conda_pkgs import HERE, DIST, RESOURCES, SPECS, h, get_version + +logger = getLogger('BuildInstallers') +logger.addHandler(h) +logger.setLevel('INFO') + +APP = "Spyder" +SPYREPO = HERE.parent +WINDOWS = os.name == "nt" +MACOS = sys.platform == "darwin" +LINUX = sys.platform.startswith("linux") +TARGET_PLATFORM = os.environ.get("CONSTRUCTOR_TARGET_PLATFORM") +PY_VER = f"{sys.version_info.major}.{sys.version_info.minor}" + +if TARGET_PLATFORM == "osx-arm64": + ARCH = "arm64" +else: + ARCH = (platform.machine() or "generic").lower().replace("amd64", "x86_64") +if WINDOWS: + EXT, OS = "exe", "Windows" +elif LINUX: + EXT, OS = "sh", "Linux" +elif MACOS: + EXT, OS = "pkg", "macOS" +else: + raise RuntimeError(f"Unrecognized OS: {sys.platform}") + +scientific_packages = { + "cython": "", + "matplotlib": "", + "numpy": "", + "openpyxl": "", + "pandas": "", + "scipy": "", + "sympy": "", +} + +# ---- Parse arguments +p = ArgumentParser() +p.add_argument( + "--no-local", action="store_true", + help="Do not use local conda packages" +) +p.add_argument( + "--debug", action="store_true", + help="Do not delete build files" +) +p.add_argument( + "--arch", action="store_true", + help="Print machine architecture tag and exit.", +) +p.add_argument( + "--ext", action="store_true", + help="Print installer extension for this platform and exit.", +) +p.add_argument( + "--artifact-name", action="store_true", + help="Print computed artifact name and exit.", +) +p.add_argument( + "--extra-specs", nargs="+", default=[], + help="One or more extra conda specs to add to the installer", +) +p.add_argument( + "--licenses", action="store_true", + help="Post-process licenses AFTER having built the installer. " + "This must be run as a separate step.", +) +p.add_argument( + "--images", action="store_true", + help="Generate background images from the logo (test only)", +) +p.add_argument( + "--cert-id", default=None, + help="Apple Developer ID Application certificate common name." +) +p.add_argument( + "--lite", action="store_true", + help=f"Do not include packages {scientific_packages.keys()}" +) +args = p.parse_args() + +yaml = YAML() +yaml.indent(mapping=2, sequence=4, offset=2) +indent4 = partial(indent, prefix=" ") + +SPYVER = get_version(SPYREPO).strip().split("+")[0] + +specs = { + "spyder": "=" + SPYVER, + "paramiko": "", + "pyxdg": "", +} + +if SPECS.exists(): + logger.info(f"Reading specs from {SPECS}...") + _specs = yaml.load(SPECS.read_text()) + specs.update(_specs) +else: + logger.info(f"Did not read specs from {SPECS}") + +if not args.lite: + specs.update(scientific_packages) + +for spec in args.extra_specs: + k, *v = re.split('([<>= ]+)', spec) + specs[k] = "".join(v).strip() + if k == "spyder": + SPYVER = v[-1] + +OUTPUT_FILE = DIST / f"EXPERIMENTAL-{APP}-{SPYVER}-{OS}-{ARCH}.{EXT}" +INSTALLER_DEFAULT_PATH_STEM = f"{APP}-{SPYVER}" + + +def _generate_background_images(installer_type): + """This requires Pillow.""" + if installer_type == "sh": + # shell installers are text-based, no graphics + return + + from PIL import Image + + logo_path = SPYREPO / "img_src" / "spyder.png" + logo = Image.open(logo_path, "r") + + if installer_type in ("exe", "all"): + sidebar = Image.new("RGBA", (164, 314), (0, 0, 0, 0)) + sidebar.paste(logo.resize((101, 101)), (32, 180)) + output = DIST / "spyder_164x314.png" + sidebar.save(output, format="png") + + banner = Image.new("RGBA", (150, 57), (0, 0, 0, 0)) + banner.paste(logo.resize((44, 44)), (8, 6)) + output = DIST / "spyder_150x57.png" + banner.save(output, format="png") + + if installer_type in ("pkg", "all"): + _logo = Image.new("RGBA", logo.size, "WHITE") + _logo.paste(logo, mask=logo) + background = Image.new("RGBA", (1227, 600), (0, 0, 0, 0)) + background.paste(_logo.resize((148, 148)), (95, 418)) + output = DIST / "spyder_1227x600.png" + background.save(output, format="png") + + +def _get_condarc(): + # we need defaults for tensorflow and others on windows only + defaults = "- defaults" if WINDOWS else "" + prompt = "[spyder]({default_env}) " + contents = dedent( + f""" + channels: #!final + - conda-forge + {defaults} + repodata_fns: #!final + - repodata.json + auto_update_conda: false #!final + notify_outdated_conda: false #!final + channel_priority: strict #!final + env_prompt: '{prompt}' #! final + """ + ) + # the undocumented #!final comment is explained here + # https://www.anaconda.com/blog/conda-configuration-engine-power-users + file = DIST / "condarc" + file.write_text(contents) + + return str(file) + + +def _definitions(): + condarc = _get_condarc() + definitions = { + "name": APP, + "company": "Spyder-IDE", + "reverse_domain_identifier": "org.spyder-ide.Spyder", + "version": SPYVER, + "channels": [ + "napari/label/bundle_tools", + "conda-forge", + ], + "conda_default_channels": ["conda-forge"], + "specs": [ + "python", + "conda", + "mamba", + "pip", + ], + "installer_filename": OUTPUT_FILE.name, + "initialize_by_default": False, + "license_file": str(RESOURCES / "bundle_license.rtf"), + "extra_envs": { + f"spyder-{SPYVER}": { + "specs": [k + v for k, v in specs.items()], + }, + }, + "menu_packages": [ + "spyder", + ], + "extra_files": { + str(RESOURCES / "bundle_readme.md"): "README.txt", + condarc: ".condarc", + }, + } + + if not args.no_local: + definitions["channels"].insert(0, "local") + + if LINUX: + definitions["default_prefix"] = os.path.join( + "$HOME", ".local", INSTALLER_DEFAULT_PATH_STEM + ) + definitions["license_file"] = str(SPYREPO / "LICENSE.txt") + definitions["installer_type"] = "sh" + + if MACOS: + welcome_text_tmpl = \ + (RESOURCES / "osx_pkg_welcome.rtf.tmpl").read_text() + welcome_file = DIST / "osx_pkg_welcome.rtf" + welcome_file.write_text( + welcome_text_tmpl.replace("__VERSION__", SPYVER)) + + # These two options control the default install location: + # ~// + definitions.update( + { + "pkg_name": INSTALLER_DEFAULT_PATH_STEM, + "default_location_pkg": "Library", + "installer_type": "pkg", + "welcome_image": str(DIST / "spyder_1227x600.png"), + "welcome_file": str(welcome_file), + "conclusion_text": "", + "readme_text": "", + "post_install": str(RESOURCES / "post-install.sh"), + } + ) + + if args.cert_id: + definitions["signing_identity_name"] = args.cert_id + definitions["notarization_identity_name"] = args.cert_id + + if WINDOWS: + definitions["conda_default_channels"].append("defaults") + definitions.update( + { + "welcome_image": str(DIST / "spyder_164x314.png"), + "header_image": str(DIST / "spyder_150x57.png"), + "icon_image": str(SPYREPO / "img_src" / "spyder.ico"), + "register_python_default": False, + "default_prefix": os.path.join( + "%LOCALAPPDATA%", INSTALLER_DEFAULT_PATH_STEM + ), + "default_prefix_domain_user": os.path.join( + "%LOCALAPPDATA%", INSTALLER_DEFAULT_PATH_STEM + ), + "default_prefix_all_users": os.path.join( + "%ALLUSERSPROFILE%", INSTALLER_DEFAULT_PATH_STEM + ), + "check_path_length": False, + "installer_type": "exe", + } + ) + + signing_certificate = os.environ.get("CONSTRUCTOR_SIGNING_CERTIFICATE") + if signing_certificate: + definitions["signing_certificate"] = signing_certificate + + if definitions.get("welcome_image") or definitions.get("header_image"): + _generate_background_images(definitions.get("installer_type", "all")) + + return definitions + + +def _constructor(): + """ + Create a temporary `construct.yaml` input file and + run `constructor`. + """ + constructor = find_executable("constructor") + if not constructor: + raise RuntimeError("Constructor must be installed and in PATH.") + + definitions = _definitions() + + cmd_args = [constructor, "-v", "--output-dir", str(DIST)] + if args.debug: + cmd_args.append("--debug") + conda_exe = os.environ.get("CONSTRUCTOR_CONDA_EXE") + if TARGET_PLATFORM and conda_exe: + cmd_args += ["--platform", TARGET_PLATFORM, "--conda-exe", conda_exe] + cmd_args.append(str(DIST)) + + env = os.environ.copy() + env["CONDA_CHANNEL_PRIORITY"] = "strict" + + logger.info("Command: " + " ".join(cmd_args)) + logger.info("Configuration:") + yaml.dump(definitions, sys.stdout) + + yaml.dump(definitions, DIST / "construct.yaml") + + check_call(cmd_args, env=env) + + +def licenses(): + info_path = DIST / "info.json" + try: + info = json.load(info_path) + except FileNotFoundError: + print( + "!! Use `constructor --debug` to write info.json and get licenses", + file=sys.stderr, + ) + raise + + zipname = DIST / f"licenses.{OS}-{ARCH}.zip" + output_zip = zipfile.ZipFile(zipname, mode="w", + compression=zipfile.ZIP_DEFLATED) + output_zip.write(info_path) + for package_id, license_info in info["_licenses"].items(): + package_name = package_id.split("::", 1)[1] + for license_type, license_files in license_info.items(): + for i, license_file in enumerate(license_files, 1): + arcname = (f"{package_name}.{license_type.replace(' ', '_')}" + f".{i}.txt") + output_zip.write(license_file, arcname=arcname) + output_zip.close() + return zipname.resolve() + + +def main(): + t0 = time() + try: + DIST.mkdir(exist_ok=True) + _constructor() + assert Path(OUTPUT_FILE).exists() + logger.info(f"Created {OUTPUT_FILE}") + finally: + elapse = timedelta(seconds=int(time() - t0)) + logger.info(f"Build time: {elapse}") + + +if __name__ == "__main__": + if args.arch: + print(ARCH) + sys.exit() + if args.ext: + print(EXT) + sys.exit() + if args.artifact_name: + print(OUTPUT_FILE) + sys.exit() + if args.licenses: + print(licenses()) + sys.exit() + if args.images: + _generate_background_images() + sys.exit() + + main() diff --git a/installers-conda/certkeychain.sh b/installers-conda/certkeychain.sh new file mode 100755 index 00000000000..be4893425a4 --- /dev/null +++ b/installers-conda/certkeychain.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -e + +CERTFILE=certificate.p12 +KEY_PASS=keypass +KEYCHAIN=build.keychain +KEYCHAINFILE=$HOME/Library/Keychains/$KEYCHAIN-db + +help(){ cat <&1 +log(){ + level="INFO" + date "+%Y-%m-%d %H:%M:%S [$level] [keychain] -> $1" 1>&3 +} + +cleanup(){ + log "Removing $CERTFILE and $KEYCHAINFILE..." + rm -f $CERTFILE + rm -f $KEYCHAINFILE +} + +while getopts "hc" option; do + case $option in + (h) help; exit ;; + (c) cleanup; exit ;; + esac +done +shift $(($OPTIND - 1)) + +[[ $# < 2 ]] && log "Password and certificate(s) not provided" && exit 1 +PASS=$1; shift +CERTS=($@) + +# ---- Remove existing keychain +if [[ -e $KEYCHAINFILE ]]; then + log "Removing existing $KEYCHAINFILE..." + security delete-keychain $KEYCHAIN +fi + +# --- Create keychain +log "Creating keychain $KEYCHAINFILE..." +security create-keychain -p $KEY_PASS $KEYCHAIN +security list-keychains -s $KEYCHAIN +security unlock-keychain -p $KEY_PASS $KEYCHAIN + +log "Importing certificate(s)..." +args=("-k" "$KEYCHAIN" "-P" "$PASS" "-T" "/usr/bin/codesign" "-T" "/usr/bin/productsign") +for cert in ${CERTS[@]}; do + if [[ -e $cert ]]; then + log "Importing cert file $cert..." + _cert=$cert + else + log "Decoding/importing base64 cert..." + echo $cert | base64 --decode > $CERTFILE + _cert=$CERTFILE + fi + security import $_cert ${args[@]} +done + +# Ensure that applications can access the cert without GUI prompt +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEY_PASS $KEYCHAIN + +# verify import +log "Verifying identity..." +security find-identity -p codesigning -v $KEYCHAIN diff --git a/installers-conda/notarize.sh b/installers-conda/notarize.sh new file mode 100755 index 00000000000..77855a431dc --- /dev/null +++ b/installers-conda/notarize.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +help(){ cat <&1 # Additional output descriptor for logging +log(){ + level="INFO" + date "+%Y-%m-%d %H:%M:%S [$level] [notarize] -> $1" 1>&3 +} + +notarize_args=("--apple-id" "mrclary@me.com") +pwd_args=("-p" "spyder-ide") +while getopts "ht:p:v" option; do + case $option in + (h) help; exit ;; + (t) notarize_args+=("--timeout" "$OPTARG") ;; + (p) pwd_args=("--password" "$OPTARG") ;; + (v) notarize_args+=("--verbose") ;; + esac +done +shift $(($OPTIND - 1)) + +[[ $# = 0 ]] && log "File not provided" && exit 1 + +PKG=$(cd $(dirname $1) && pwd -P)/$(basename $1) # Resolve full path + +# --- Get certificate id +CNAME=$(security find-identity -p codesigning -v | pcregrep -o1 "\(([0-9A-Z]+)\)") +[[ -z $CNAME ]] && log "Could not locate certificate ID" && exit 1 +log "Certificate ID: $CNAME" + +notarize_args+=("--team-id" "$CNAME" "${pwd_args[@]}") + +# --- Notarize +log "Notarizing..." +xcrun notarytool submit $PKG --wait ${notarize_args[@]} | tee temp.txt + +submitid=$(pcregrep -o1 "^\s*id: ([0-9a-z-]+)" temp.txt | head -1) +status=$(pcregrep -o1 "^\s*status: (\w+$)" temp.txt) +rm temp.txt + +xcrun notarytool log $submitid ${notarize_args[@]} + +if [[ "$status" != "Accepted" ]]; then + log "Notarizing failed!" + exit 1 +fi + +log "Stapling notary ticket..." +xcrun stapler staple -v "$PKG" +if [[ $? != 0 ]]; then + log "Stapling failed!" + exit 1 +fi diff --git a/installers-conda/resources/Spyder.sh b/installers-conda/resources/Spyder.sh new file mode 100644 index 00000000000..679e11553cb --- /dev/null +++ b/installers-conda/resources/Spyder.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Get user environment variables +eval "$($SHELL -l -c "declare -x")" + +# Activate the conda environment +source $ROOT_PREFIX/bin/activate $PREFIX + +# Find root conda and mamba +export PATH=$ROOT_PREFIX/condabin:$PATH + +# Launch Spyder +$(dirname "$0")/python $CONDA_PREFIX/bin/spyder "$@" diff --git a/installers-conda/resources/build-patch.sh b/installers-conda/resources/build-patch.sh new file mode 100644 index 00000000000..ff69f9d5fb4 --- /dev/null +++ b/installers-conda/resources/build-patch.sh @@ -0,0 +1,14 @@ +mkdir -p "${PREFIX}/Menu" +sed "s/__PKG_VERSION__/${PKG_VERSION}/" "${SRC_DIR}/installers-conda/resources/spyder-menu.json" > "${PREFIX}/Menu/spyder-menu.json" +cp "${SRC_DIR}/img_src/spyder.png" "${PREFIX}/Menu/spyder.png" +cp "${SRC_DIR}/img_src/spyder.icns" "${PREFIX}/Menu/spyder.icns" +cp "${SRC_DIR}/img_src/spyder.ico" "${PREFIX}/Menu/spyder.ico" + +if [[ $OSTYPE = "darwin"* ]]; then + if [[ -z $(which shc) ]]; then + echo "Installing shc shell script compiler..." + brew install shc + fi + echo "Compiling Spyder.sh..." + shc -r -f "${SRC_DIR}/installers-conda/resources/Spyder.sh" -o "${PREFIX}/Menu/Spyder" +fi diff --git a/installers-conda/resources/bundle_license.rtf b/installers-conda/resources/bundle_license.rtf new file mode 100644 index 00000000000..6889a5c1111 --- /dev/null +++ b/installers-conda/resources/bundle_license.rtf @@ -0,0 +1,21 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2639 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid1\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} +{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} +\margl1440\margr1440\vieww19860\viewh17800\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 MIT License\ +\ +Copyright (c) 2009- Spyder Project Contributors and others (see AUTHORS.txt); the spyder/images dir and some source files under other terms (see NOTICE.txt)\ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ +\pard\tx220\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\li720\fi-720\pardirnatural\partightenfactor0 +\ls1\ilvl0\cf0 \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ +} \ No newline at end of file diff --git a/installers-conda/resources/bundle_readme.md b/installers-conda/resources/bundle_readme.md new file mode 100644 index 00000000000..ac6bd600792 --- /dev/null +++ b/installers-conda/resources/bundle_readme.md @@ -0,0 +1,56 @@ +Welcome to the Spyder installation contents +------------------------------------------- + +This is the base installation of Spyder, the Scientific Python Development Environment. + +## How do I run Spyder? + +In most cases, you would run it through the platform-specific shortcut we created for your +convenience. In other words, _not_ through this directory! + +* Linux: Check your desktop launcher. +* MacOS: Check `~/Applications` or the Launchpad. +* Windows: Check the Start Menu or the Desktop. + +We generally recommend using the shortcut because it will pre-activate the `conda` environment for +you! That said, you can also execute the `spyder` executable directly from these locations: + +* Linux and macOS: find it under `bin`, next to this file. +* Windows: navigate to `Scripts`, next to this file. + +In unmodified installations, this _should_ be enough to launch `spyder`, but sometimes you will +need to activate the `conda` environment to ensure all dependencies are importable. + +## What does `conda` have to do with `spyder`? + +The Spyder installer uses `conda` packages to bundle all its dependencies (Python, Qt, etc). +This directory is actually a full `conda` installation! If you have used `conda` before, this +is equivalent to what you usually call the `base` environment. + +## Can I modify the `spyder` installation? + +Yes, but it is not recommended (see below). In practice, you can consider it a `conda` environment. You can even activate it as usual, +provided you specify the full path to the location, instead of the _name_. + +``` +# macOS +$ conda activate ~/Library/spyder-x.y.z +# Linux +$ conda activate ~/.local/spyder-x.y.z +# Windows +$ conda activate %LOCALAPPDATA%/spyder-x.y.z +``` + +Then you will be able to run `conda` and `pip` as usual. That said, we advise against this advanced +manipulation. It can render `spyder` unusable if not done carefully! You might need to reinstall it +in that case. + +## What is `_conda.exe`? + +This executable is a full `conda` installation, condensed in a single file. It allows us to handle +the installation in a more robust way. It also provides a way to restore destructive changes without +reinstalling anything. Again, consider this an advanced tool only meant for expert debugging. + +## More information + +Check our online documentation at https://www.spyder-ide.org/ diff --git a/installers-conda/resources/installers-conda.patch b/installers-conda/resources/installers-conda.patch new file mode 100644 index 00000000000..9df9e545c27 --- /dev/null +++ b/installers-conda/resources/installers-conda.patch @@ -0,0 +1,313 @@ +From 40db10418cf9b46a70b48026d202d4474d62b99a Mon Sep 17 00:00:00 2001 +From: Ryan Clary <9618975+mrclary@users.noreply.github.com> +Date: Mon, 12 Sep 2022 23:56:56 -0700 +Subject: [PATCH 1/2] Revise usage of running_in_mac_app + +--- + spyder/app/restart.py | 14 +++----- + spyder/app/utils.py | 1 + + spyder/config/base.py | 36 +++++-------------- + .../providers/languageserver/provider.py | 6 +--- + .../ipythonconsole/utils/kernelspec.py | 2 ++ + .../plugins/profiler/widgets/main_widget.py | 7 +--- + spyder/plugins/pylint/main_widget.py | 7 +--- + spyder/utils/programs.py | 5 +-- + spyder/utils/pyenv.py | 2 +- + 9 files changed, 22 insertions(+), 58 deletions(-) + +diff --git a/spyder/app/restart.py b/spyder/app/restart.py +index b85dd03cb..6bc566e89 100644 +--- a/spyder/app/restart.py ++++ b/spyder/app/restart.py +@@ -27,7 +27,7 @@ + + # Local imports + from spyder.app.utils import create_splash_screen +-from spyder.config.base import _, running_in_mac_app ++from spyder.config.base import _ + from spyder.utils.image_path_manager import get_image_path + from spyder.utils.encoding import to_unicode + from spyder.utils.qthelpers import qapplication +@@ -228,16 +228,12 @@ def main(): + args_reset = ['--reset'] + + # Build the base command +- if running_in_mac_app(sys.executable): +- exe = env['EXECUTABLEPATH'] +- command = [f'"{exe}"'] ++ if is_bootstrap: ++ script = osp.join(spyder_dir, 'bootstrap.py') + else: +- if is_bootstrap: +- script = osp.join(spyder_dir, 'bootstrap.py') +- else: +- script = osp.join(spyder_dir, 'spyder', 'app', 'start.py') ++ script = osp.join(spyder_dir, 'spyder', 'app', 'start.py') + +- command = [f'"{sys.executable}"', f'"{script}"'] ++ command = [f'"{sys.executable}"', f'"{script}"'] + + # Adjust the command and/or arguments to subprocess depending on the OS + shell = not IS_WINDOWS +diff --git a/spyder/app/utils.py b/spyder/app/utils.py +index 254df98b4..700ec6d13 100644 +--- a/spyder/app/utils.py ++++ b/spyder/app/utils.py +@@ -311,6 +311,7 @@ def create_window(WindowClass, app, splash, options, args): + QCoreApplication.setAttribute(Qt.AA_DontShowIconsInMenus, True) + + # Open external files with our Mac app ++ # ??? Do we need this? + if running_in_mac_app(): + app.sig_open_external_file.connect(main.open_external_file) + app._has_started = True +diff --git a/spyder/config/base.py b/spyder/config/base.py +index c0b9a4b29..204a6d72c 100644 +--- a/spyder/config/base.py ++++ b/spyder/config/base.py +@@ -549,42 +549,24 @@ def translate_gettext(x): + #============================================================================== + # Mac application utilities + #============================================================================== +-def running_in_mac_app(pyexec=None): ++def running_in_mac_app(pyexec=sys.executable): + """ +- Check if Python executable is located inside a standalone Mac app. ++ Check if Spyder is running as a macOS bundle app. ++ Checks for SPYDER_APP environment variable to determine this. + +- If no executable is provided, the default will check `sys.executable`, i.e. +- whether Spyder is running from a standalone Mac app. +- +- This is important for example for the single_instance option and the +- interpreter status in the statusbar. ++ If python executable is provided, checks if it is the same as the macOS ++ bundle app environment executable. + """ +- if pyexec is None: +- pyexec = sys.executable +- +- bpath = get_mac_app_bundle_path() ++ # Spyder is macOS app ++ mac_app = os.environ.get('SPYDER_APP') is not None + +- if bpath and pyexec == osp.join(bpath, 'Contents/MacOS/python'): ++ if mac_app and pyexec == sys.executable: ++ # executable is macOS app + return True + else: + return False + + +-def get_mac_app_bundle_path(): +- """ +- Return the full path to the macOS app bundle. Otherwise return None. +- +- EXECUTABLEPATH environment variable only exists if Spyder is a macOS app +- bundle. In which case it will always end with +- "/.app/Conents/MacOS/Spyder". +- """ +- app_exe_path = os.environ.get('EXECUTABLEPATH', None) +- if sys.platform == "darwin" and app_exe_path: +- return osp.dirname(osp.dirname(osp.dirname(osp.abspath(app_exe_path)))) +- else: +- return None +- +- + # ============================================================================= + # Micromamba + # ============================================================================= +diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py +index e8959e13a..199a82b08 100644 +--- a/spyder/plugins/completion/providers/languageserver/provider.py ++++ b/spyder/plugins/completion/providers/languageserver/provider.py +@@ -23,8 +23,7 @@ + # Local imports + from spyder.api.config.decorators import on_conf_change + from spyder.utils.installers import InstallerPylspError +-from spyder.config.base import (_, get_conf_path, running_under_pytest, +- running_in_mac_app) ++from spyder.config.base import _, get_conf_path, running_under_pytest + from spyder.config.lsp import PYTHON_CONFIG + from spyder.utils.misc import check_connection_port + from spyder.plugins.completion.api import (SUPPORTED_LANGUAGES, +@@ -819,9 +818,6 @@ def generate_python_config(self): + else: + environment = self.get_conf('executable', + section='main_interpreter') +- # External interpreter cannot have PYTHONHOME +- if running_in_mac_app(): +- env_vars.pop('PYTHONHOME', None) + + jedi = { + 'environment': environment, +diff --git a/spyder/plugins/ipythonconsole/utils/kernelspec.py b/spyder/plugins/ipythonconsole/utils/kernelspec.py +index 0acdce9a9..4bdc4edd8 100644 +--- a/spyder/plugins/ipythonconsole/utils/kernelspec.py ++++ b/spyder/plugins/ipythonconsole/utils/kernelspec.py +@@ -187,6 +187,7 @@ def env(self): + env_vars['SPY_RUN_CYTHON'] = True + + # App considerations ++ # ??? Do we need this? + if (running_in_mac_app() or is_pynsist()): + if default_interpreter: + # See spyder-ide/spyder#16927 +@@ -194,6 +195,7 @@ def env(self): + # See spyder-ide/spyder#17552 + env_vars['PYDEVD_DISABLE_FILE_VALIDATION'] = 1 + else: ++ # ??? Do we need this? + env_vars.pop('PYTHONHOME', None) + + # Remove this variable because it prevents starting kernels for +diff --git a/spyder/plugins/profiler/widgets/main_widget.py b/spyder/plugins/profiler/widgets/main_widget.py +index 91c411bdf..c274f5872 100644 +--- a/spyder/plugins/profiler/widgets/main_widget.py ++++ b/spyder/plugins/profiler/widgets/main_widget.py +@@ -34,7 +34,7 @@ + from spyder.api.translations import get_translation + from spyder.api.widgets.main_widget import PluginMainWidget + from spyder.api.widgets.mixins import SpyderWidgetMixin +-from spyder.config.base import get_conf_path, running_in_mac_app ++from spyder.config.base import get_conf_path + from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor + from spyder.py3compat import to_text_string + from spyder.utils.misc import get_python_executable, getcwd_or_home +@@ -534,11 +534,6 @@ def start(self, wdir=None, args=None, pythonpath=None): + + executable = self.get_conf('executable', section='main_interpreter') + +- if not running_in_mac_app(executable): +- env = self.process.processEnvironment() +- env.remove('PYTHONHOME') +- self.process.setProcessEnvironment(env) +- + self.output = '' + self.error_output = '' + self.running = True +diff --git a/spyder/plugins/pylint/main_widget.py b/spyder/plugins/pylint/main_widget.py +index ffe47d6a8..811ed1458 100644 +--- a/spyder/plugins/pylint/main_widget.py ++++ b/spyder/plugins/pylint/main_widget.py +@@ -31,7 +31,7 @@ + from spyder.api.config.decorators import on_conf_change + from spyder.api.translations import get_translation + from spyder.api.widgets.main_widget import PluginMainWidget +-from spyder.config.base import get_conf_path, is_pynsist, running_in_mac_app ++from spyder.config.base import get_conf_path, is_pynsist + from spyder.config.utils import is_anaconda + from spyder.plugins.pylint.utils import get_pylintrc_path + from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor +@@ -378,11 +378,6 @@ def _start(self): + if not is_pynsist() and not is_anaconda(): + processEnvironment.insert("APPDATA", os.environ.get("APPDATA")) + +- # resolve spyder-ide/spyder#14262 +- if running_in_mac_app(): +- pyhome = os.environ.get("PYTHONHOME") +- processEnvironment.insert("PYTHONHOME", pyhome) +- + process.setProcessEnvironment(processEnvironment) + process.start(sys.executable, command_args) + running = process.waitForStarted() +diff --git a/spyder/utils/programs.py b/spyder/utils/programs.py +index 3c6c03e35..b640f4a6f 100644 +--- a/spyder/utils/programs.py ++++ b/spyder/utils/programs.py +@@ -30,8 +30,7 @@ + import psutil + + # Local imports +-from spyder.config.base import (running_under_pytest, get_home_dir, +- running_in_mac_app) ++from spyder.config.base import running_under_pytest, get_home_dir + from spyder.utils import encoding + from spyder.utils.misc import get_python_executable + +@@ -774,8 +773,6 @@ def run_python_script_in_terminal(fname, wdir, args, interact, debug, + delete=False) + if wdir: + f.write('cd "{}"\n'.format(wdir)) +- if running_in_mac_app(executable): +- f.write(f'export PYTHONHOME={os.environ["PYTHONHOME"]}\n') + if pypath is not None: + f.write(f'export PYTHONPATH={pypath}\n') + f.write(' '.join([executable] + p_args)) +diff --git a/spyder/utils/pyenv.py b/spyder/utils/pyenv.py +index 49f894e7c..018e02130 100644 +--- a/spyder/utils/pyenv.py ++++ b/spyder/utils/pyenv.py +@@ -11,7 +11,7 @@ + import os + import os.path as osp + +-from spyder.config.base import get_home_dir, running_in_mac_app ++from spyder.config.base import get_home_dir + from spyder.utils.programs import find_program, run_shell_command + + +-- +2.37.3 + + +From 01bd81747e570df7d9be7a50f0589809e58cf5d4 Mon Sep 17 00:00:00 2001 +From: Ryan Clary <9618975+mrclary@users.noreply.github.com> +Date: Mon, 12 Sep 2022 23:58:36 -0700 +Subject: [PATCH 2/2] Update standalone conda executable. + +--- + spyder/config/base.py | 5 +++-- + spyder/plugins/ipythonconsole/scripts/conda-activate.sh | 4 ++-- + spyder/utils/conda.py | 4 ++-- + 3 files changed, 7 insertions(+), 6 deletions(-) + +diff --git a/spyder/config/base.py b/spyder/config/base.py +index 204a6d72c..2629026d8 100644 +--- a/spyder/config/base.py ++++ b/spyder/config/base.py +@@ -573,8 +573,9 @@ def running_in_mac_app(pyexec=sys.executable): + def get_spyder_umamba_path(): + """Return the path to the Micromamba executable bundled with Spyder.""" + if running_in_mac_app(): +- path = osp.join(osp.dirname(osp.dirname(__file__)), +- 'bin', 'micromamba') ++ # TODO: Change to CONDA_EXE when ++ # conda-forge/conda-standalone-feedstock#45 is resolved ++ path = os.environ.get('CONDA_PYTHON_EXE') + elif is_pynsist(): + path = osp.abspath(osp.join(osp.dirname(osp.dirname(__file__)), + 'bin', 'micromamba.exe')) +diff --git a/spyder/plugins/ipythonconsole/scripts/conda-activate.sh b/spyder/plugins/ipythonconsole/scripts/conda-activate.sh +index f2243cfcc..0d92d4205 100755 +--- a/spyder/plugins/ipythonconsole/scripts/conda-activate.sh ++++ b/spyder/plugins/ipythonconsole/scripts/conda-activate.sh +@@ -8,8 +8,8 @@ CONDA_ENV_PYTHON=$3 + SPYDER_KERNEL_SPEC=$4 + + # Activate kernel environment +-if [[ "$CONDA_ACTIVATE_SCRIPT" = *"micromamba" ]]; then +- eval "$($CONDA_ACTIVATE_SCRIPT shell activate -p $CONDA_ENV_PATH)" ++if [[ "$CONDA_ACTIVATE_SCRIPT" = *"_conda.exe" ]]; then ++ eval "$($CONDA_ACTIVATE_SCRIPT shell.bash activate $CONDA_ENV_PATH)" + else + source $CONDA_ACTIVATE_SCRIPT $CONDA_ENV_PATH + fi +diff --git a/spyder/utils/conda.py b/spyder/utils/conda.py +index 2c43c37dc..e8db8a4f5 100644 +--- a/spyder/utils/conda.py ++++ b/spyder/utils/conda.py +@@ -73,8 +73,8 @@ def get_conda_activation_script(quote=False): + # Use micromamba bundled with Spyder installers or find conda exe + exe = get_spyder_umamba_path() or find_conda() + +- if osp.basename(exe).startswith('micromamba'): +- # For Micromamba, use the executable ++ if osp.basename(exe) in ('micromamba.exe', '_conda.exe'): ++ # For stadalone conda, use the executable + script_path = exe + else: + # Conda activation script is relative to executable +-- +2.37.3 + diff --git a/installers-conda/resources/osx_pkg_welcome.rtf.tmpl b/installers-conda/resources/osx_pkg_welcome.rtf.tmpl new file mode 100644 index 00000000000..8ab63459689 --- /dev/null +++ b/installers-conda/resources/osx_pkg_welcome.rtf.tmpl @@ -0,0 +1,15 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2580 +\cocoascreenfonts1\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 LucidaGrande;} +{\colortbl;\red255\green255\blue255;\red60\green64\blue68;\red255\green255\blue255;} +{\*\expandedcolortbl;;\cssrgb\c30196\c31765\c33725;\cssrgb\c100000\c100000\c100000;} +\margl1440\margr1440\vieww12040\viewh13780\viewkind0 +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 + +\f0\fs28 \cf0 Thanks for choosing Spyder v__VERSION__!\ +\ +{\field{\*\fldinst{HYPERLINK "https://www.spyder-ide.org"}}{\fldrslt Spyder}} is the Scientific Python Development Environment.\ +\ +The installation will begin shortly.\ +\ +If at any point an error is shown, please save the logs (\uc0\u8984+L) before closing the installer and submit the resulting file along with your report in {\field{\*\fldinst{HYPERLINK "https://github.com/spyder-ide/spyder/issues"}}{\fldrslt our issue tracker}}. Thank you!\ +} \ No newline at end of file diff --git a/installers-conda/resources/post-install.sh b/installers-conda/resources/post-install.sh new file mode 100755 index 00000000000..a1771860f20 --- /dev/null +++ b/installers-conda/resources/post-install.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -e + +echo "*** Starting post install script for __NAME__.app" + +cat < /dev/null + +PREFIX=$(cd "$2/__NAME_LOWER__"; pwd) +ROOT_PREFIX=$(cd "$PREFIX/../../"; pwd) + +if [[ "$PREFIX" == "$HOME"* ]]; then + # Installed for user + app_path="$HOME/Applications/Spyder.app" +else + # Installed for all users + app_path="/Applications/Spyder.app" +fi + +# Delete the application +if [[ -e "$app_path" ]]; then + echo "Removing $app_path..." + rm -r "$app_path" +fi + +# Delete the environment +if [[ -e "$ROOT_PREFIX" ]]; then + echo "Removing $ROOT_PREFIX" + rm -r "$ROOT_PREFIX" +fi + +echo "*** Pre install script for Spyder installer complete" diff --git a/installers-conda/resources/spyder-menu.json b/installers-conda/resources/spyder-menu.json new file mode 100644 index 00000000000..befd8373403 --- /dev/null +++ b/installers-conda/resources/spyder-menu.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://schemas.conda.io/menuinst-1.schema.json", + "menu_name": "spyder", + "menu_items": [ + { + "name": "Spyder", + "description": "Spyder description", + "icon": "{{ MENU_DIR }}/spyder.{{ ICON_EXT }}", + "command": ["spyder", "$@"], + "activate": true, + "terminal": false, + "platforms": { + "win": { + "desktop": true + }, + "linux": { + "Categories": [ + "Graphics", + "Science" + ] + }, + "osx": { + "CFBundleName": "Spyder", + "CFBundleDisplayName": "Spyder", + "CFBundleIdentifier": "org.spyder-ide.Spyder", + "CFBundleVersion": "__PKG_VERSION__" + } + } + } + ] +} diff --git a/setup.cfg b/setup.cfg index d2baa0a8849..753120781e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,7 @@ ignore = img_src/*.xcf img_src/*.pdn img_src/*.icns - installers/** + installers*/** pytest.ini requirements/** rope_profiling/**