diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 332b443f76..c156659b33 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -31,7 +31,7 @@ jobs: - build: macos-arm64 os: macos-latest arch: arm64 - macos_target: 'MACOSX_DEPLOYMENT_TARGET=11 CARGO_BUILD_TARGET=aarch64-apple-darwin' + macos_target: 'MACOSX_DEPLOYMENT_TARGET=11.0 CARGO_BUILD_TARGET=aarch64-apple-darwin' fail-fast: false steps: diff --git a/.github/workflows/dev_envs.yml b/.github/workflows/dev_envs.yml index c53aa38695..1cdf1fffb8 100644 --- a/.github/workflows/dev_envs.yml +++ b/.github/workflows/dev_envs.yml @@ -11,25 +11,14 @@ jobs: with: fetch-depth: 0 - - name: Cache nix store - id: cache-nix - uses: actions/cache@v3 + - uses: cachix/install-nix-action@v18 with: - path: | - ~/.nix-portable/store - ~/bin/nix-portable - key: nix-${{ hashFiles('shell.nix') }}-${{ hashFiles('nix/**') }}-v009 - - - name: Install nix-portable - if: steps.cache-nix.outputs.cache-hit != 'true' - run: | - mkdir ~/bin - wget -qO ~/bin/nix-portable https://github.com/DavHau/nix-portable/releases/download/v009/nix-portable - chmod +x ~/bin/nix-portable + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - run: ~/bin/nix-portable nix-shell --command "tox -e py39" + - run: nix run .# -- --version - - run: ~/bin/nix-portable nix run . -- --version + - run: nix-shell --command "tox -e py39" mamba: runs-on: ubuntu-latest diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c97a62e6bf..a67a75c130 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,4 +1,4 @@ -# note: to invalidate caches, adjust the pip-v? and tox-v? numbers below. +# note: to invalidate caches, adjust the pip-v? number below. name: Python tests on: @@ -14,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-22.04, macos-latest] - py: ["3.10", "3.9", "3.8"] + py: ["3.11", "3.10", "3.9", "3.8"] fail-fast: false steps: @@ -36,7 +36,7 @@ jobs: uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-v4-${{ hashFiles('**/setup.cfg') }} + key: ${{ runner.os }}-pip-v4-${{ hashFiles('**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip-v4- @@ -65,7 +65,7 @@ jobs: uses: actions/cache@v3 with: path: .tox/ - key: ${{ runner.os }}-tox-v4-${{ hashFiles('**/setup.cfg') }} + key: ${{ runner.os }}-tox-v4-${{ hashFiles('**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-tox-v4- diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 32bf8e195a..2cb8c93feb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -268,26 +268,17 @@ jobs: - uses: actions-rs/toolchain@v1 with: - toolchain: 1.48.0 + toolchain: 1.56.1 override: true - name: check if README matches MSRV defined here - run: grep '1.48.0' src/core/README.md - - - name: Set up Python 3.8 - uses: actions/setup-python@v4 - with: - python-version: "3.8" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e . + run: grep '1.56.1' src/core/README.md - name: Check if it builds properly uses: actions-rs/cargo@v1 with: command: build + args: --all-features check_cbindgen: name: "Check if cbindgen runs cleanly for generating the C headers" diff --git a/.gitignore b/.gitignore index 968cc71d2e..23035e78c9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ src/sourmash/_lowlevel*.py Pipfile Pipfile.lock target/ -Cargo.lock .eggs .asv pkg/ diff --git a/asv.conf.json b/asv.conf.json index d5687509ba..4b6770bc35 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -11,8 +11,7 @@ "html_dir": ".asv/html", "build_cache_size": 8, "build_command": [ - "python -m pip install 'setuptools_scm[toml]>=4,<6' milksnake", - "python setup.py build", + "python -m pip install 'setuptools_scm[toml]>=4,<6' milksnake maturin", "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" ] } diff --git a/flake.lock b/flake.lock index a84e6788de..766da3bb24 100644 --- a/flake.lock +++ b/flake.lock @@ -1,47 +1,5 @@ { "nodes": { - "flake-utils": { - "locked": { - "lastModified": 1642700792, - "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "mach-nix": { - "inputs": { - "flake-utils": [ - "utils" - ], - "nixpkgs": [ - "nixpkgs" - ], - "pypi-deps-db": [ - "pypi-deps-db" - ] - }, - "locked": { - "lastModified": 1654084003, - "narHash": "sha256-j/XrVVistvM+Ua+0tNFvO5z83isL+LBgmBi9XppxuKA=", - "owner": "DavHau", - "repo": "mach-nix", - "rev": "7e14360bde07dcae32e5e24f366c83272f52923f", - "type": "github" - }, - "original": { - "owner": "DavHau", - "ref": "3.5.0", - "repo": "mach-nix", - "type": "github" - } - }, "naersk": { "inputs": { "nixpkgs": [ @@ -49,11 +7,11 @@ ] }, "locked": { - "lastModified": 1662220400, - "narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=", + "lastModified": 1671096816, + "narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=", "owner": "nix-community", "repo": "naersk", - "rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3", + "rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114", "type": "github" }, "original": { @@ -64,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1664028844, - "narHash": "sha256-wwGqnvROHW54ma0h4q6GL5toKxTVVKvAypv0CcJkraU=", + "lastModified": 1671458120, + "narHash": "sha256-2+k/OONN4OF21TeoNjKB5sXVZv6Zvm/uEyQIW9OYCg8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "72bdd03f0d5696412b25a93218acaad530570d30", + "rev": "e37ef84b478fa8da0ced96522adfd956fde9047a", "type": "github" }, "original": { @@ -78,64 +36,10 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1643805626, - "narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "554d2d8aa25b6e583575459c297ec23750adb6cb", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-unstable", - "type": "indirect" - } - }, - "pypi-deps-db": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2", - "pypi-deps-db": "pypi-deps-db_2" - }, - "locked": { - "lastModified": 1654084003, - "narHash": "sha256-j/XrVVistvM+Ua+0tNFvO5z83isL+LBgmBi9XppxuKA=", - "owner": "DavHau", - "repo": "mach-nix", - "rev": "7e14360bde07dcae32e5e24f366c83272f52923f", - "type": "github" - }, - "original": { - "owner": "DavHau", - "ref": "3.5.0", - "repo": "mach-nix", - "type": "github" - } - }, - "pypi-deps-db_2": { - "flake": false, - "locked": { - "lastModified": 1643877077, - "narHash": "sha256-jv8pIvRFTP919GybOxXE5TfOkrjTbdo9QiCO1TD3ZaY=", - "owner": "DavHau", - "repo": "pypi-deps-db", - "rev": "da53397f0b782b0b18deb72ef8e0fb5aa7c98aa3", - "type": "github" - }, - "original": { - "owner": "DavHau", - "repo": "pypi-deps-db", - "type": "github" - } - }, "root": { "inputs": { - "mach-nix": "mach-nix", "naersk": "naersk", "nixpkgs": "nixpkgs", - "pypi-deps-db": "pypi-deps-db", "rust-overlay": "rust-overlay", "utils": "utils" } @@ -150,11 +54,11 @@ ] }, "locked": { - "lastModified": 1664074880, - "narHash": "sha256-/V1TX4HLADElvi3MuuIbNdvzR/HmNzbYRemKBjX/5YY=", + "lastModified": 1671503041, + "narHash": "sha256-PJdh3q1zzzaMzk3M6yZHUVqQSBPiF46EVfMh7L+jtEI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "45140fa526b1cb85498f717e355c79a54367cb1d", + "rev": "e4e129cf743b49b663d99abe81142d6bd213ac91", "type": "github" }, "original": { @@ -165,11 +69,11 @@ }, "utils": { "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index c70f48ca34..745fcbb0cc 100644 --- a/flake.nix +++ b/flake.nix @@ -16,28 +16,32 @@ url = "github:nix-community/naersk"; inputs = { nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "utils"; }; }; - - mach-nix = { - url = "github:DavHau/mach-nix/3.5.0"; - inputs.nixpkgs.follows = "nixpkgs"; - inputs.flake-utils.follows = "utils"; - inputs.pypi-deps-db.follows = "pypi-deps-db"; - }; - - pypi-deps-db = { - url = "github:DavHau/mach-nix/3.5.0"; - }; }; - outputs = { self, nixpkgs, naersk, rust-overlay, mach-nix, pypi-deps-db, utils }: + outputs = { self, nixpkgs, naersk, rust-overlay, utils }: utils.lib.eachDefaultSystem (system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs { inherit system overlays; + config.packageOverrides = pkgs: + { + maturin = pkgs.rustPlatform.buildRustPackage rec { + pname = "maturin"; + version = "0.14.7"; + src = pkgs.fetchFromGitHub { + owner = "PyO3"; + repo = "maturin"; + rev = "v0.14.7"; + hash = "sha256-PCE4SvUrS8ass8UPc+t5Ix126Q4tB2yCMU2kWuCfr5Q="; + }; + cargoHash = "sha256-ODMOJOoyra29ZeaG0yKnjPRwcjh/20VsgOz+IGZbQ/s="; + nativeBuildInputs = [ pkgs.pkg-config ]; + doCheck = false; + }; + }; }; rustVersion = pkgs.rust-bin.stable.latest.default.override { #extensions = [ "rust-src" ]; @@ -53,42 +57,58 @@ rustc = rustPlatform.rust.rustc; }; - python = "python39"; - mach-nix-wrapper = import mach-nix { inherit pkgs python; }; + python = pkgs.python310Packages; + + screed = python.buildPythonPackage rec { + pname = "screed"; + version = "1.1"; + #format = "pyproject"; + + src = pkgs.fetchFromGitHub { + owner = "dib-lab"; + repo = "screed"; + rev = "v1.1"; + hash = "sha256-g1FZJx94RGBPoTiLfwttdYqCJ02pxtOKK708WA63kHE="; + }; + + SETUPTOOLS_SCM_PRETEND_VERSION = "1.1"; + propagatedBuildInputs = with python; [ setuptools bz2file setuptools_scm ]; + doCheck = false; + }; + in with pkgs; { packages = { + lib = naersk-lib.buildPackage { pname = "libsourmash"; root = ./.; copyLibs = true; }; - sourmash = mach-nix-wrapper.buildPythonPackage { - src = ./.; + + sourmash = python.buildPythonPackage rec { pname = "sourmash"; - version = "4.4.2"; - requirements = '' - screed>=1.0.5 - cffi>=1.14.0 - numpy - matplotlib - scipy - deprecation>=2.0.6 - cachetools<6,>=4 - bitstring<5,>=3.1.9 - ''; - requirementsExtra = '' - setuptools >= 61 - milksnake - setuptools_scm[toml] >= 4, <6 - wheel >= 0.29.0 - ''; - SETUPTOOLS_SCM_PRETEND_VERSION = "4.4.2"; + version = "4.6.1"; + format = "pyproject"; + + src = ./.; + + cargoDeps = rustPlatform.fetchCargoTarball { + inherit src; + name = "${pname}-${version}"; + hash = "sha256-IaIX4RdXEhLQhne+QiSfdP1KiIwZUqxRg14uWknS+0o="; + }; + + nativeBuildInputs = with rustPlatform; [ cargoSetupHook maturinBuildHook ]; + + buildInputs = lib.optionals stdenv.isDarwin [ libiconv ]; + propagatedBuildInputs = with python; [ cffi deprecation cachetools bitstring numpy scipy matplotlib screed ]; + DYLD_LIBRARY_PATH = "${self.packages.${system}.lib}/lib"; - NO_BUILD = "1"; }; + docker = let bin = self.defaultPackage.${system}; @@ -119,11 +139,13 @@ git stdenv.cc.cc.lib - (python310.withPackages (ps: with ps; [ virtualenv tox setuptools ])) - (python39.withPackages (ps: with ps; [ virtualenv setuptools ])) - (python38.withPackages (ps: with ps; [ virtualenv setuptools ])) + (python310.withPackages (ps: with ps; [ virtualenv tox cffi ])) + (python311.withPackages (ps: with ps; [ virtualenv ])) + (python39.withPackages (ps: with ps; [ virtualenv ])) + (python38.withPackages (ps: with ps; [ virtualenv ])) rust-cbindgen + maturin wasmtime wasm-pack diff --git a/pyproject.toml b/pyproject.toml index 6a8b247f82..8f87a4ca5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,15 @@ [build-system] requires = [ - "setuptools >= 61", - "setuptools_scm[toml] >= 4, <6", - "setuptools_scm_git_archive", - "milksnake", - "wheel >= 0.29.0", + "maturin>=0.14.7,<0.15", + "cffi", ] -build-backend = 'setuptools.build_meta' +build-backend = 'maturin' [project] name = "sourmash" description = "tools for comparing biological sequences with k-mer sketches" readme = "README.md" +version = "4.6.1" authors = [ { name="Luiz Irber", orcid="0000-0003-4371-9659" }, @@ -83,7 +81,6 @@ dependencies = [ ] requires-python = ">=3.8" -dynamic = ["version"] [metadata] license = { text = "BSD 3-Clause License" } @@ -130,19 +127,15 @@ storage = [ # https://github.com/pypa/pip/issues/10393#issuecomment-941885429 all = ["sourmash[test,demo,doc,storage]"] -[tool.setuptools] -zip-safe = false -platforms = ["any"] - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.setuptools.dynamic] -version = {attr = "sourmash.version.version"} - -[tool.setuptools_scm] -write_to = "src/sourmash/version.py" -git_describe_command = "git describe --dirty --tags --long --match v* --first-parent" +[tool.maturin] +python-source = "src" +manifest-path = "src/core/Cargo.toml" +bindings = "cffi" +include = [ + { path = "include/sourmash.h", format = ["sdist","wheel"] }, +] +features = ["maturin"] +locked = true [tool.isort] known_third_party = ["deprecation", "hypothesis", "mmh3", "numpy", "pkg_resources", "pytest", "screed", "setuptools", "sourmash_tst_utils"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 8d3cb339bd..0000000000 --- a/setup.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import sys - -from setuptools import setup - - -DEBUG_BUILD = os.environ.get("SOURMASH_DEBUG") == "1" -NO_BUILD = os.environ.get("NO_BUILD") == "1" - - -def find_dylib_no_build(name, paths): - to_find = None - if sys.platform == 'darwin': - to_find = f'lib{name}.dylib' - elif sys.platform == 'win32': - to_find = f'{name}.dll' - else: - to_find = f'lib{name}.so' - - for path in paths.split(":"): - for filename in os.listdir(path): - if filename == to_find: - return os.path.join(path, filename) - - raise LookupError('dylib %r not found' % name) - - -def find_dylib(build, target): - cargo_target = os.environ.get("CARGO_BUILD_TARGET") - if cargo_target: - in_path = "target/%s/%s" % (cargo_target, target) - else: - in_path = "target/%s" % target - return build.find_dylib("sourmash", in_path=in_path) - - -def build_native(spec): - cmd = ["cargo", "build", - "--manifest-path", "src/core/Cargo.toml", - # "--features", "parallel", - "--lib"] - - target = "debug" - if not DEBUG_BUILD: - cmd.append("--release") - target = "release" - - if NO_BUILD: - dylib = lambda: find_dylib_no_build("sourmash", os.environ["DYLD_LIBRARY_PATH"]) - header_filename = lambda: "include/sourmash.h" - else: - build = spec.add_external_build(cmd=cmd, path=".") - dylib = lambda: find_dylib(build, target) - header_filename=lambda: build.find_header("sourmash.h", in_path="include") - - rtld_flags = ["NOW"] - if sys.platform == "darwin": - rtld_flags.append("NODELETE") - spec.add_cffi_module( - module_path="sourmash._lowlevel", - dylib=dylib, - header_filename=header_filename, - rtld_flags=rtld_flags, - ) - -setup( - milksnake_tasks=[build_native], - package_dir={"": "src"}, -) diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index b9742b1372..820a60868e 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -7,10 +7,11 @@ repository = "https://github.com/sourmash-bio/sourmash" keywords = ["minhash", "bioinformatics"] categories = ["science", "algorithms", "data-structures"] license = "BSD-3-Clause" -edition = "2018" +edition = "2021" readme = "README.md" autoexamples = false autobins = false +rust-version = "1.56.1" [lib] name = "sourmash" @@ -20,6 +21,7 @@ bench = false [features] from-finch = ["finch"] parallel = ["rayon"] +maturin = [] [dependencies] az = "1.0.0" @@ -89,3 +91,6 @@ wasm-bindgen-test = "0.3.31" ### These crates don't compile on wasm [target.'cfg(not(all(target_arch = "wasm32", target_vendor="unknown")))'.dependencies] + +[package.metadata.maturin] +name = "sourmash._lowlevel" diff --git a/src/core/README.md b/src/core/README.md index 005d3334e5..0aee799654 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -38,4 +38,4 @@ Development happens on github at ## Minimum supported Rust version -Currently the minimum supported Rust version is 1.48.0. +Currently the minimum supported Rust version is 1.56.1. diff --git a/src/core/build.rs b/src/core/build.rs new file mode 100644 index 0000000000..a22396c25a --- /dev/null +++ b/src/core/build.rs @@ -0,0 +1,75 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + copy_c_bindings(&crate_dir); +} + +#[cfg(not(feature = "maturin"))] +fn copy_c_bindings(_crate_dir: &str) {} + +#[cfg(feature = "maturin")] +fn copy_c_bindings(crate_dir: &str) { + use std::path::{Path, PathBuf}; + + fn find_root_dir(crate_dir: &str) -> &Path { + let root_dir = Path::new(crate_dir); + + if root_dir.join("pyproject.toml").is_file() { + return root_dir; + } + + let root_dir = Path::new(crate_dir).parent().unwrap().parent().unwrap(); + if root_dir.join("pyproject.toml").is_file() { + return root_dir; + } + + panic!("Couldn't find pyproject.toml to determine root dir"); + } + + fn find_target_dir(out_dir: &str) -> PathBuf { + use std::ffi::OsStr; + + let mut components = Path::new(out_dir).iter(); + + while let Some(dir) = components.next_back() { + if dir == OsStr::new("target") { + break; + } + } + let mut dir: PathBuf = components.collect(); + + if dir.as_os_str().is_empty() { + panic!("Couldn't find target dir based on OUT_DIR"); + } else { + dir.push("target"); + dir + } + } + + let root_dir = find_root_dir(crate_dir); + let header_path = root_dir.join("include").join("sourmash.h"); + let header = std::fs::read_to_string(header_path).expect("error reading header"); + + // strip directives, not supported by the cffi C parser + let new_header: String = header + .lines() + .filter_map(|s| { + if s.starts_with("#") { + None + } else { + Some({ + let mut s = s.to_owned(); + s.push_str("\n"); + s + }) + } + }) + .collect(); + + let out_dir = env::var("OUT_DIR").unwrap(); + let target_dir = find_target_dir(&out_dir); + std::fs::create_dir_all(&target_dir).expect("error creating target dir"); + let out_path = target_dir.join("header.h"); + std::fs::write(out_path, &new_header).expect("error writing header"); +} diff --git a/src/sourmash/cli/sbt_combine.py b/src/sourmash/cli/sbt_combine.py index 2e708b928d..1b5ce0febf 100644 --- a/src/sourmash/cli/sbt_combine.py +++ b/src/sourmash/cli/sbt_combine.py @@ -11,16 +11,6 @@ def subparser(subparsers): '-x', '--bf-size', metavar='S', type=float, default=1e5 ) - subparser = subparsers.add_parser('sbt_combine') - subparser.add_argument('sbt_name', help='name to save SBT into') - subparser.add_argument( - 'sbts', nargs='+', - help='SBTs to combine to form a new SBT' - ) - subparser.add_argument( - '-x', '--bf-size', metavar='S', type=float, default=1e5 - ) - def main(args): import sourmash diff --git a/src/sourmash/cli/tax/metagenome.py b/src/sourmash/cli/tax/metagenome.py index 0bc7ab7c4e..df81789360 100644 --- a/src/sourmash/cli/tax/metagenome.py +++ b/src/sourmash/cli/tax/metagenome.py @@ -27,7 +27,6 @@ def subparser(subparsers): subparser = subparsers.add_parser('metagenome', - aliases=['summarize'], usage=usage) subparser.add_argument( '-g', '--gather-csv', action="extend", nargs='*', default = [], diff --git a/tox.ini b/tox.ini index 6a8d681ad8..87e4330004 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] envlist = py310, + py311, py39, coverage, codecov, @@ -209,6 +210,7 @@ source = src/sourmash/ [gh-actions] python = 3.10: py310, docs, package_description, coverage, codecov + 3.11: py311, coverage, codecov 3.9: py39, coverage, codecov 3.8: py38, coverage, codecov