diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1bafe93..bd026d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,6 +34,7 @@ jobs: pip install . - name: Build Python package + if: matrix.os == 'ubuntu-latest' run: python -m build - name: Publish to PyPI @@ -53,16 +54,69 @@ jobs: sudo apt-get install -y ruby ruby-dev build-essential rpm fakeroot tar libarchive-tools sudo gem install --no-document fpm make deb pacman rpm - + - name: Install Rosetta (if needed) + if: matrix.os == 'macos-latest' + run: | + sudo /usr/sbin/softwareupdate --install-rosetta --agree-to-license || true + - name: Build universal binary with lipo (arm64 + x86_64) + if: matrix.os == 'macos-latest' + run: | + mkdir -p dist-universal + cp dist/xcsp dist-universal/xcsp-arm64 + arch -x86_64 pyinstaller --onefile --name xcsp-x86_64 --paths=. bin/main.py + cp dist/xcsp-x86_64 dist-universal/xcsp-x86_64 + lipo -create dist-universal/xcsp-arm64 dist-universal/xcsp-x86_64 -output dist-universal/xcsp + - name: Import Apple certificate + if: matrix.os == 'macos-latest' + run: | + echo "$CERT_P12_BASE64" | base64 --decode > certificate.p12 + security create-keychain -p "" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "" build.keychain + security import certificate.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain + env: + CERT_P12_BASE64: ${{ secrets.CERT_P12_BASE64 }} + CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }} + - name: Sign macOS binary + if: matrix.os == 'macos-latest' + run: | + codesign --timestamp --options runtime \ + --sign "Developer ID Application" \ + dist-universal/xcsp + - name: Notarize macOS binary with Apple + if: matrix.os == 'macos-latest' + run: | + xcrun notarytool submit dist-universal/xcsp \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PWD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PWD: ${{ secrets.APPLE_APP_SPECIFIC_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + - name: Staple notarization ticket + if: matrix.os == 'macos-latest' + run: | + xcrun stapler staple dist-universal/xcsp + - name: Package signed universal binary for Homebrew + if: matrix.os == 'macos-latest' + run: | + mkdir -p brew_tmp/bin brew_tmp/share/xcsp-launcher/configs brew_tmp/share/xcsp-launcher/tools + cp dist-universal/xcsp brew_tmp/bin/xcsp-macos + cp -r configs/* brew_tmp/share/xcsp-launcher/configs/ + cp xcsp/tools/xcsp3-solutionChecker-2.5.jar brew_tmp/share/xcsp-launcher/tools/ + tar -czvf xcsp-$(git describe --tags --abbrev=0)-macos.tar.gz -C brew_tmp . - name: Homebrew (macOS only) if: matrix.os == 'macos-latest' run: | - make brew + sha256=$(shasum -a 256 xcsp-$(git describe --tags --abbrev=0)-macos.tar.gz | awk '{print $1}') + url="https://github.com/CPToolset/xcsp-launcher/releases/download/$(git describe --tags --abbrev=0)/xcsp-$(git describe --tags --abbrev=0)-macos.tar.gz" + sed -e "s|__URL__|$url|" -e "s|__SHASUM__|$sha256|" .packaging/homebrew/xcsp.rb.template > .packaging/homebrew/xcsp.rb git clone --quiet https://x-access-token:${{ secrets.XCSP_GITHUB_TOKEN }}@github.com/CPToolset/homebrew-xcsp-launcher.git brew-tap - mkdir -p brew-tap/Formula/ cp .packaging/homebrew/xcsp.rb brew-tap/Formula/xcsp.rb - cd brew-tap && git add Formula/ && git commit -m "Update formula for version $(VERSION)" && git push - rm -rf brew-tap + cd brew-tap && git add Formula/xcsp.rb && git commit -m "Update formula for version $(git describe --tags --abbrev=0)" && git push env: GITHUB_TOKEN: ${{ secrets.XCSP_GITHUB_TOKEN }} @@ -86,7 +140,7 @@ jobs: *.deb *.rpm *.pkg.tar.* - *.tar.gz + xcsp-*-macos.tar.gz *.snap chocolatey/*.nupkg diff --git a/Makefile b/Makefile index 100e6e3..6778d36 100644 --- a/Makefile +++ b/Makefile @@ -49,39 +49,39 @@ deb: $(DIST_DIR)/$(BIN_NAME) cp package/*.deb . rm -rf package -# Créer une Formula Homebrew à partir du tar.gz -brew: $(DIST_DIR)/$(BIN_NAME) - @echo "Building Homebrew formula..." - - # Générer un tar.gz contenant juste l'exécutable et les configs - mkdir -p brew_tmp/bin brew_tmp/share/xcsp-launcher/configs brew_tmp/share/xcsp-launcher/tools - cp $(DIST_DIR)/$(BIN_NAME) brew_tmp/bin/xcsp-macos - cp $(DIST_DIR)/${BIN_NAME} $(DIST_DIR)/xcsp-macos - cp -r configs/* brew_tmp/share/xcsp-launcher/configs/ - cp xcsp/tools/xcsp3-solutionChecker-2.5.jar brew_tmp/share/xcsp-launcher/tools/xcsp3-solutionChecker-2.5.jar - - # Créer archive - tar -czvf xcsp-$(VERSION:v%=%)-macos.tar.gz -C brew_tmp . - - { \ - sha256=$$(shasum -a 256 xcsp-$(VERSION:v%=%)-macos.tar.gz | awk '{print $$1}'); \ - url="https://github.com/CPToolset/xcsp-launcher/releases/download/$(VERSION)/xcsp-$(VERSION:v%=%)-macos.tar.gz"; \ - sed \ - -e "s|__URL__|$$url|" \ - -e "s|__SHASUM__|$$sha256|" \ - .packaging/homebrew/xcsp.rb.template > .packaging/homebrew/xcsp.rb; \ - } - - # Nettoyer temporaire - rm -rf brew_tmp - -publish-brew: xcsp-*-macos.tar.gz - @echo "Publishing Homebrew Formula..." - git clone https://github.com/CPToolset/homebrew-xcsp-launcher.git brew-tap - mkdir -p brew-tap/Formula/ - cp .packaging/homebrew/xcsp.rb brew-tap/Formula/xcsp.rb - cd brew-tap && git add Formula/ && git commit -m "Update formula for version $(VERSION)" && git push - rm -rf brew-tap +# # Créer une Formula Homebrew à partir du tar.gz +# brew: $(DIST_DIR)/$(BIN_NAME) +# @echo "Building Homebrew formula..." +# +# # Générer un tar.gz contenant juste l'exécutable et les configs +# mkdir -p brew_tmp/bin brew_tmp/share/xcsp-launcher/configs brew_tmp/share/xcsp-launcher/tools +# cp $(DIST_DIR)/$(BIN_NAME) brew_tmp/bin/xcsp-macos +# cp $(DIST_DIR)/${BIN_NAME} $(DIST_DIR)/xcsp-macos +# cp -r configs/* brew_tmp/share/xcsp-launcher/configs/ +# cp xcsp/tools/xcsp3-solutionChecker-2.5.jar brew_tmp/share/xcsp-launcher/tools/xcsp3-solutionChecker-2.5.jar +# +# # Créer archive +# tar -czvf xcsp-$(VERSION:v%=%)-macos.tar.gz -C brew_tmp . +# +# { \ +# sha256=$$(shasum -a 256 xcsp-$(VERSION:v%=%)-macos.tar.gz | awk '{print $$1}'); \ +# url="https://github.com/CPToolset/xcsp-launcher/releases/download/$(VERSION)/xcsp-$(VERSION:v%=%)-macos.tar.gz"; \ +# sed \ +# -e "s|__URL__|$$url|" \ +# -e "s|__SHASUM__|$$sha256|" \ +# .packaging/homebrew/xcsp.rb.template > .packaging/homebrew/xcsp.rb; \ +# } +# +# # Nettoyer temporaire +# rm -rf brew_tmp +# +# publish-brew: xcsp-*-macos.tar.gz +# @echo "Publishing Homebrew Formula..." +# git clone https://github.com/CPToolset/homebrew-xcsp-launcher.git brew-tap +# mkdir -p brew-tap/Formula/ +# cp .packaging/homebrew/xcsp.rb brew-tap/Formula/xcsp.rb +# cd brew-tap && git add Formula/ && git commit -m "Update formula for version $(VERSION)" && git push +# rm -rf brew-tap pacman: $(DIST_DIR)/$(BIN_NAME) mkdir -p package/usr/bin diff --git a/configs b/configs index 0df3ccc..16585d2 160000 --- a/configs +++ b/configs @@ -1 +1 @@ -Subproject commit 0df3ccca5a8ab12dbdc620fe2e9a83eecceb99bc +Subproject commit 16585d2b5f9d67a0ced660b90a31a85be52d11ff diff --git a/tests/test_installed_solvers.py b/tests/test_installed_solvers.py index f1fe615..a367d52 100644 --- a/tests/test_installed_solvers.py +++ b/tests/test_installed_solvers.py @@ -39,9 +39,9 @@ def test_with_xcsp_file_sat(self, solver, instance): print(f"{solver} is not present in solutions.json file. We skip test.",file=sys.stderr) return solution_for_current_instance = solutions[solver][instance.split("/")[-1]] - print(f"Test of ace with input {instance}", file=sys.stderr) + print(f"Test of {solver} with input {instance}", file=sys.stderr) for index, o in enumerate(solution_for_current_instance["solutions"]): - solver = Solver.lookup("ace") + solver = Solver.lookup(solver) solver.set_limit_number_of_solutions(index + 1) solver.solve(instance) assert solver.objective_value() is not None @@ -59,7 +59,7 @@ def test_with_xcsp_file_sat(self, solver, instance): (solver, instance) for solver in solvers for instance in instances_unsat ]) def test_with_xcsp_file_unsat(self, solver, instance): - print(f"Test of ace with input {instance}", file=sys.stderr) + print(f"Test of {solver} with input {instance}", file=sys.stderr) solver = Solver.lookup(solver) solver.solve(instance) assert solver.objective_value() is None @@ -69,7 +69,7 @@ def test_with_xcsp_file_unsat(self, solver, instance): (solver, instance) for solver in solvers for instance in instances_unknown ]) def test_with_xcsp_file_unknown(self, solver, instance): - print(f"Test of ace with input {instance}", file=sys.stderr) + print(f"Test of {solver} with input {instance}", file=sys.stderr) solver = Solver.lookup(solver) solver.set_time_limit(10) solver.solve(instance) diff --git a/xcsp/commands/install.py b/xcsp/commands/install.py index 5a71c3b..472cbb2 100644 --- a/xcsp/commands/install.py +++ b/xcsp/commands/install.py @@ -20,6 +20,8 @@ from loguru import logger from timeit import default_timer as timer +from packaging.version import Version + from xcsp.builder.build import AutoBuildStrategy, ManualBuildStrategy from xcsp.builder.check import check_available_builder_for_language, MAP_FILE_LANGUAGE, MAP_LANGUAGE_FILES, MAP_BUILDER from xcsp.utils.archive import ALL_ARCHIVE_EXTENSIONS, extract_archive @@ -135,6 +137,17 @@ def build_cmd(config, bin_executable, bin_dir): return result_cmd +def keep_only_semver_versions(all_versions): + results = [] + for v in all_versions: + try: + _ = Version(v) + results.append(v) + except Exception as e: + continue + return sort_versions(results) + + class Installer: """Main class responsible for installing a solver from a repository.""" @@ -144,7 +157,7 @@ def __init__(self, url: str, solver_name: str, id_s: str, config=None): self._id = id_s self._path_solver = None self._start_time = timer() - self._repo = None + self._repo: VersionDirectory = None self._config = config self._config_strategy = None self._mode_build_strategy = None @@ -222,7 +235,7 @@ def _manage_dependency(self): def _manage_git_dependency(self, dep, git_url): name = git_url.split("/")[-1].replace(".git", "") default_dir = self._path_solver.parent.parent / "deps" / name - target_dir = replace_solver_dir_in_str(dep.get("dir"), str(self._repo.get_source_path)) if dep.get( + target_dir = replace_solver_dir_in_str(dep.get("dir"), str(self._repo.get_source_path())) if dep.get( "dir") else default_dir target_dir = Path(target_dir) target_dir.parent.mkdir(parents=True, exist_ok=True) @@ -327,13 +340,26 @@ def install(self): f"Please manually copy your binaries into {bin_dir}.") continue executable_path = Path(v['executable']) - result_path = shutil.copy(Path(self._repo.get_source_path()) / v["executable"], - bin_dir / executable_path.name) - logger.success(f"Executable for version '{v['version']}' successfully copied to {result_path}.") + final_placeholder_for_executable= executable_path.name + if executable_path.is_dir(): + logger.info(f"Copying content of directory '{executable_path}' to binary directory '{bin_dir}'.") + for item in (Path(self._repo.get_source_path()) / executable_path).iterdir(): + dest = bin_dir / item.name + shutil.copy(item.absolute(), dest) + final_placeholder_for_executable = bin_dir + logger.success(f"Directory for version '{v['version']}' successfully copied to {bin_dir}.") + elif executable_path.is_file(): + result_path = shutil.copy(Path(self._repo.get_source_path()) / v["executable"], + bin_dir / executable_path.name) + final_placeholder_for_executable = bin_dir/executable_path.name + logger.success(f"Executable for version '{v['version']}' successfully copied to {result_path}.") + if self._config is not None and self._config.get("command") is not None: + result_cmd = build_cmd(self._config, final_placeholder_for_executable , bin_dir) + logger.debug(result_cmd) CACHE[self._id]["versions"][v['version']] = { "options": self._config["command"].get("options", dict()), - "cmd": build_cmd(self._config, bin_dir / executable_path.name, bin_dir), + "cmd": build_cmd(self._config, final_placeholder_for_executable , bin_dir), "alias": v.get("alias", list()) } have_latest = have_latest or "latest" in v.get("alias", []) or v.get("version") == "latest" or v.get("git_tag") == "latest" @@ -362,7 +388,8 @@ def install(self): logger.info(f"Restoring original repository (if needed)...") self._repo.restore() logger.info(f"Version '{v['version']}' end ... {timer() - version_timer:.2f} seconds.") - list_versions = sort_versions(list(CACHE[self._id]["versions"].keys())) + all_versions = list(CACHE[self._id]["versions"].keys()) + list_versions = keep_only_semver_versions(all_versions) if not have_latest and len(list_versions)>0: latest = list_versions[-1] logger.debug(list_versions)