Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support adding custom env vars #504

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Conversation

konstin
Copy link
Member

@konstin konstin commented Jan 1, 2025

Currently, maturin and setuptools-rust are not self-contained: You need to install rust on the machine in order to build a package. We can improve this by downloading a matching rustup, using it to install cargo and rustc into a temporary directory and using this rust for the build. As long as the platform is supported by rustup, we get a self-contained build that doesn't mess with the host system. You can for example use pip install ... in a fresh python3-slim docker container and any rust dependency with a missing wheel will build and install without additional setup.

To inject this cache installation, we need to set PATH, RUST_HOME and CARGO_HOME whenever we call cargo or rustc.

With these changes, we can make bootstrapping maturin self-contained:

if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
    from puccinialin import setup_rust

    print("Rust not found, installing into a temporary directory")
    extra_env = setup_rust()
    env = {**os.environ, **extra_env}
else:
    env = None

setup(
    version=version,
    cmdclass={"bdist_wheel": bdist_wheel},
    rust_extensions=[RustBin("maturin", args=cargo_args, cargo_manifest_args=["--locked"], env=env)],
    zip_safe=False,
)
"""Support installing rust before compiling (bootstrapping) maturin.

Installing a package that uses maturin as build backend on a platform without maturin
binaries, we install rust in a cache directory if the user doesn't have a rust
installation already. Since this bootstrapping requires more dependencies but is only
required if rust is missing, we check if cargo is present before requesting those
dependencies.

https://setuptools.pypa.io/en/stable/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks
"""

from __future__ import annotations

import os
import shutil
from typing import Any

# noinspection PyUnresolvedReferences
from setuptools.build_meta import *  # noqa:F403


def get_requires_for_build_wheel(_config_settings: dict[str, Any] = None) -> list[str]:
    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
        return ["puccinialin"]
    return []


def get_requires_for_build_sdist(_config_settings: dict[str, Any] = None) -> list[str]:
    if not os.environ.get("MATURIN_NO_INSTALL_RUST") and not shutil.which("cargo"):
        return ["puccinialin"]
    return []

@konstin konstin force-pushed the konsti/add-custom-env-vars branch 3 times, most recently from f1d788f to 0c7bebc Compare January 1, 2025 20:42
@konstin
Copy link
Member Author

konstin commented Jan 1, 2025

The mingw CI failures are also on main

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Implementation looks fine, though I slightly worry about the complexity.

I tried to look for prior art, as far as I can tell Cython / setup tools doesn't have a supported option and just leaves you to modify os.environ as part of setup.py. Is there a reason why that's not good enough for this use case here?

@davidhewitt
Copy link
Member

(Mingw failures I've just resolved in #501. PyPy 3.10 is just flat out broken as far as I can tell until the next PyPy version is released.)

@konstin
Copy link
Member Author

konstin commented Jan 3, 2025

I tried to look for prior art, as far as I can tell Cython / setup tools doesn't have a supported option and just leaves you to modify os.environ as part of setup.py. Is there a reason why that's not good enough for this use case here?

os.environ is global state and i'd prefer to isolate those values, as far as that's possible for environment variables. In my experience that helps with debugging.

Mingw failures I've just resolved in #501. PyPy 3.10 is just flat out broken as far as I can tell until the next PyPy version is released.

Thanks I'll rebase.

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.environ is global state and i'd prefer to isolate those values, as far as that's possible for environment variables. In my experience that helps with debugging.

I agree with this, though I am also willing to be a bit more lax for simplicity's sake in these "build system" components especially when it's sort of ok (IMO) to treat setup.py as an entrypoint setting up build configuration.

I guess the main cost of the isolation (as we see in this PR) is that we have to be careful to pass the env dict to all downstream processes; forgetting to do so once is probably easy to miss in review and will create bugs. Can we use ruff to forbid use of subprocess outside our own private wrapper which requires an Env? (Maybe this setting?)

Is there any place outside of subprocesses where these variables might matter? I think it seems clear enough from docs that we only intend for this to affect rustc / cargo. The only other subprocesses we call (as far as I see from a quick scan) are strip and lipo (on various platforms), but I think there is little need for those to be affected by this option.

I think as long as we have ways to avoid this getting out of sync later with some tests or lints, this gets 👍 from me.

setuptools_rust/setuptools_ext.py Outdated Show resolved Hide resolved
@konstin konstin force-pushed the konsti/add-custom-env-vars branch from 0c7bebc to d00026c Compare January 10, 2025 20:33
@konstin
Copy link
Member Author

konstin commented Jan 10, 2025

Rebase to fix the tests and fixed the review comment

@davidhewitt
Copy link
Member

I guess the main cost of the isolation (as we see in this PR) is that we have to be careful to pass the env dict to all downstream processes; forgetting to do so once is probably easy to miss in review and will create bugs. Can we use ruff to forbid use of subprocess outside our own private wrapper which requires an Env? (Maybe this setting?)

Any ideas what we can do to mitigate this concern? Does my suggestion to use ruff seem viable?

@konstin
Copy link
Member Author

konstin commented Jan 13, 2025

That's a though problem, i can't figure out a good solution: The subprocess module has a number of functions that create a subprocess, and we don't want to ban using them, we only want to ensure that env= is set explicitly so that you don't accidentally forget the environment (or set env=None if it's not a rustc/cargo call). I don't see how to do that with lint.flake8-tidy-imports, did you have something specific in mind?

@cassiersg
Copy link
Contributor

As an independent use-case for this feature: I just needed this feature today: I am building two RustExtensions in a single setup(...), and the extensions need different RUSTFLAGS env vars. As far as I can tell, this is currently impossible. (I ended up monkey-patching _prepare_build_environment with some inspect magic.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants