|
| 1 | +import os |
1 | 2 | import platform |
2 | 3 | import subprocess |
| 4 | +import sys |
| 5 | + |
| 6 | + |
| 7 | +def _running_noninteractively(): |
| 8 | + """True when there is no human at a terminal to answer a prompt. |
| 9 | +
|
| 10 | + This covers CI systems (GitHub Actions sets CI=true), any environment |
| 11 | + where CI/PIP_NO_INPUT is set, and plain non-tty stdin. |
| 12 | + """ |
| 13 | + if os.environ.get("CI") or os.environ.get("PIP_NO_INPUT"): |
| 14 | + return True |
| 15 | + try: |
| 16 | + return not sys.stdin.isatty() |
| 17 | + except Exception: |
| 18 | + return True |
| 19 | + |
| 20 | + |
| 21 | +def _is_packaging_only_invocation(): |
| 22 | + """True when setup.py is only being asked to build metadata/artifacts |
| 23 | + (sdist, bdist_wheel, egg_info, ...), not to actually install the |
| 24 | + package. These commands run on build machines (e.g. CI building a |
| 25 | + release) that don't need R/rpy2 present at all -- R is a *runtime* |
| 26 | + dependency of the installed package, not a build-time one. |
| 27 | + """ |
| 28 | + packaging_commands = { |
| 29 | + "sdist", "bdist_wheel", "bdist", "egg_info", "dist_info", |
| 30 | + "check", "--version", "--help", "--help-commands", |
| 31 | + } |
| 32 | + return any(arg in packaging_commands for arg in sys.argv[1:]) |
| 33 | + |
3 | 34 |
|
4 | 35 | def check_r_installed(): |
5 | 36 | current_platform = platform.system() |
6 | 37 |
|
7 | 38 | if current_platform == "Windows": |
8 | | - # Check if R is installed on Windows by checking the registry |
9 | 39 | try: |
10 | 40 | subprocess.run( |
11 | 41 | ["reg", "query", "HKLM\\Software\\R-core\\R"], check=True |
12 | 42 | ) |
13 | 43 | print("R is already installed on Windows.") |
14 | 44 | return True |
15 | | - except subprocess.CalledProcessError: |
| 45 | + except (subprocess.CalledProcessError, FileNotFoundError): |
16 | 46 | print("R is not installed on Windows.") |
17 | 47 | return False |
18 | 48 |
|
19 | | - elif current_platform == "Linux": |
20 | | - # Check if R is installed on Linux by checking if the 'R' executable is available |
| 49 | + elif current_platform in ("Linux", "Darwin"): |
21 | 50 | try: |
22 | 51 | subprocess.run(["which", "R"], check=True) |
23 | | - print("R is already installed on Linux.") |
| 52 | + print(f"R is already installed on {current_platform}.") |
24 | 53 | return True |
25 | | - except subprocess.CalledProcessError: |
26 | | - print("R is not installed on Linux.") |
27 | | - return False |
28 | | - |
29 | | - elif current_platform == "Darwin": # macOS |
30 | | - # Check if R is installed on macOS by checking if the 'R' executable is available |
31 | | - try: |
32 | | - subprocess.run(["which", "R"], check=True) |
33 | | - print("R is already installed on macOS.") |
34 | | - return True |
35 | | - except subprocess.CalledProcessError: |
36 | | - print("R is not installed on macOS.") |
| 54 | + except (subprocess.CalledProcessError, FileNotFoundError): |
| 55 | + print(f"R is not installed on {current_platform}.") |
37 | 56 | return False |
38 | 57 |
|
39 | 58 | else: |
40 | 59 | print("Unsupported platform. Unable to check for R installation.") |
41 | 60 | return False |
42 | 61 |
|
43 | | -def install_r(): |
44 | 62 |
|
| 63 | +def install_r(): |
45 | 64 | current_platform = platform.system() |
46 | 65 |
|
47 | 66 | if current_platform == "Windows": |
48 | | - # Install R on Windows using PowerShell |
49 | | - install_command = "Start-Process powershell -Verb subprocess.runAs -ArgumentList '-Command \"& {Invoke-WebRequest https://cran.r-project.org/bin/windows/base/R-4.1.2-win.exe -OutFile R.exe}; Start-Process R.exe -ArgumentList '/SILENT' -Wait}'" |
| 67 | + install_command = ( |
| 68 | + "Start-Process powershell -Verb runAs -ArgumentList " |
| 69 | + "'-Command \"& {Invoke-WebRequest " |
| 70 | + "https://cran.r-project.org/bin/windows/base/R-4.1.2-win.exe " |
| 71 | + "-OutFile R.exe}; Start-Process R.exe -ArgumentList " |
| 72 | + "'/SILENT' -Wait}'" |
| 73 | + ) |
50 | 74 | subprocess.run(install_command, shell=True) |
51 | 75 |
|
52 | 76 | elif current_platform == "Linux": |
53 | | - # Install R on Linux using the appropriate package manager (e.g., apt-get) |
54 | 77 | install_command = ( |
55 | | - "sudo apt update -qq && sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9" |
56 | | - + "&& sudo add-apt-repository 'deb https://cloud.r-project.org/bin/linux/ubuntu focal-cran40/'" |
57 | | - + "&& sudo apt update" |
58 | | - + "&& sudo apt -y install r-base" |
| 78 | + "sudo apt update -qq && " |
| 79 | + "sudo apt-key adv --keyserver keyserver.ubuntu.com " |
| 80 | + "--recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9 && " |
| 81 | + "sudo add-apt-repository 'deb https://cloud.r-project.org/bin/linux/ubuntu focal-cran40/' && " |
| 82 | + "sudo apt update && " |
| 83 | + "sudo apt -y install r-base" |
59 | 84 | ) |
60 | 85 | subprocess.run(install_command, shell=True) |
61 | 86 |
|
62 | | - elif current_platform == "Darwin": # macOS |
63 | | - # Install R on macOS using Homebrew |
64 | | - install_command = "brew install r" |
65 | | - subprocess.run(install_command, shell=True) |
| 87 | + elif current_platform == "Darwin": |
| 88 | + subprocess.run("brew install r", shell=True) |
66 | 89 |
|
67 | 90 | else: |
68 | | - |
69 | 91 | print("Unsupported platform. Unable to install R.") |
70 | 92 |
|
| 93 | + |
71 | 94 | def install_packages(): |
72 | | - try: |
73 | | - subprocess.run(["Rscript", "-e", "utils::install.packages('remotes', dependencies=TRUE)"]) |
74 | | - subprocess.run(["Rscript", "-e", "utils::install.packages(c('R6', 'Rcpp', 'skimr'), dependencies=TRUE)"]) |
75 | | - subprocess.run(["Rscript", "-e", "remotes::install_github('Techtonique/learningmachine')"]) |
76 | | - except subprocess.CalledProcessError as e: |
77 | | - print(f"Error occurred: {e}") |
78 | | - print(f"Return code: {e.returncode}") |
79 | | - print(f"Output: {e.output}") |
80 | | - print(f"Stderr: {e.stderr}") |
| 95 | + """Install the R-side dependencies, retrying into a local library |
| 96 | + ('r-learningmachine') if the default library isn't writable.""" |
| 97 | + attempts = [ |
| 98 | + {"lib": None}, |
| 99 | + {"lib": "r-learningmachine"}, |
| 100 | + ] |
| 101 | + |
| 102 | + for attempt in attempts: |
| 103 | + lib = attempt["lib"] |
| 104 | + lib_arg = f", lib='{lib}'" if lib else "" |
81 | 105 | try: |
82 | | - subprocess.run(["mkdir", "-p", "r-learningmachine"]) |
83 | | - subprocess.run(["Rscript", "-e", "utils::install.packages('remotes', lib='r-learningmachine', dependencies=TRUE)"]) |
84 | | - subprocess.run(["Rscript", "-e", "utils::install.packages(c('R6', 'Rcpp', 'skimr'), lib='r-learningmachine', dependencies=TRUE)"]) |
85 | | - subprocess.run(["Rscript", "-e", "remotes::install_github('Techtonique/learningmachine', lib='r-learningmachine')"]) |
86 | | - except subprocess.CalledProcessError as e: |
87 | | - print(f"Error occurred: {e}") |
| 106 | + if lib: |
| 107 | + subprocess.run(["mkdir", "-p", lib], check=True) |
| 108 | + subprocess.run( |
| 109 | + ["Rscript", "-e", |
| 110 | + f"utils::install.packages('remotes', dependencies=TRUE{lib_arg})"], |
| 111 | + check=True, |
| 112 | + ) |
| 113 | + subprocess.run( |
| 114 | + ["Rscript", "-e", |
| 115 | + f"utils::install.packages(c('R6', 'Rcpp', 'skimr'), dependencies=TRUE{lib_arg})"], |
| 116 | + check=True, |
| 117 | + ) |
| 118 | + subprocess.run( |
| 119 | + ["Rscript", "-e", |
| 120 | + f"remotes::install_github('Techtonique/learningmachine'{lib_arg})"], |
| 121 | + check=True, |
| 122 | + ) |
| 123 | + print(f"R package installation succeeded (lib={lib!r}).") |
| 124 | + return True |
| 125 | + except subprocess.CalledProcessError as e: |
| 126 | + print(f"Error occurred while installing R packages (lib={lib!r}): {e}") |
88 | 127 | print(f"Return code: {e.returncode}") |
89 | | - print(f"Output: {e.output}") |
90 | | - print(f"Stderr: {e.stderr}") |
91 | | - try: |
92 | | - subprocess.run(["Rscript", "-e", "utils::install.packages('remotes', dependencies=TRUE)"]) |
93 | | - subprocess.run(["Rscript", "-e", "utils::install.packages(c('R6', 'Rcpp', 'skimr'), dependencies=TRUE)"]) |
94 | | - subprocess.run(["Rscript", "-e", "remotes::install_github('Techtonique/learningmachine')"]) |
95 | | - except subprocess.CalledProcessError as e: |
96 | | - print(f"Error occurred: {e}") |
97 | | - print(f"Return code: {e.returncode}") |
98 | | - print(f"Output: {e.output}") |
99 | | - print(f"Stderr: {e.stderr}") |
100 | | - try: |
101 | | - subprocess.run(["mkdir", "-p", "r-learningmachine"]) |
102 | | - subprocess.run(["Rscript", "-e", "utils::install.packages('remotes', lib='r-learningmachine', dependencies=TRUE)"]) |
103 | | - subprocess.run(["Rscript", "-e", "utils::install.packages(c('R6', 'Rcpp', 'skimr'), lib='r-learningmachine', dependencies=TRUE)"]) |
104 | | - subprocess.run(["Rscript", "-e", "remotes::install_github('Techtonique/learningmachine', lib='r-learningmachine')"]) |
105 | | - except subprocess.CalledProcessError as e: |
106 | | - print(f"Error occurred: {e}") |
107 | | - print(f"Return code: {e.returncode}") |
108 | | - print(f"Output: {e.output}") |
109 | | - print(f"Stderr: {e.stderr}") |
110 | | - try: |
111 | | - subprocess.run(["Rscript", "-e", "utils::install.packages('remotes', dependencies=TRUE)"]) |
112 | | - subprocess.run(["Rscript", "-e", "utils::install.packages(c('R6', 'Rcpp', 'skimr'), dependencies=TRUE)"]) |
113 | | - subprocess.run(["Rscript", "-e", "remotes::install_github('Techtonique/learningmachine')"]) |
114 | | - except subprocess.CalledProcessError as e: |
115 | | - print(f"Error occurred: {e}") |
116 | | - print(f"Return code: {e.returncode}") |
117 | | - print(f"Output: {e.output}") |
118 | | - print(f"Stderr: {e.stderr}") |
119 | | - subprocess.run(["mkdir", "-p", "r-learningmachine"]) |
120 | | - subprocess.run(["Rscript", "-e", "utils::install.packages('remotes', lib='r-learningmachine', dependencies=TRUE)"]) |
121 | | - subprocess.run(["Rscript", "-e", "utils::install.packages(c('R6', 'Rcpp', 'skimr'), lib='r-learningmachine', dependencies=TRUE)"]) |
122 | | - subprocess.run(["Rscript", "-e", "remotes::install_github('Techtonique/learningmachine', lib='r-learningmachine')"]) |
123 | | - |
124 | | - |
125 | | -# Check if R is installed; if not, install it |
126 | | -if not check_r_installed(): |
127 | | - install_r_prompt = int(input("Try installing R? 1-yes, 2-no")) |
128 | | - if install_r_prompt == 1: |
| 128 | + |
| 129 | + print( |
| 130 | + "Warning: could not install the R 'learningmachine' package " |
| 131 | + "automatically. Install it manually, e.g.:\n" |
| 132 | + " Rscript -e \"remotes::install_github('Techtonique/learningmachine')\"" |
| 133 | + ) |
| 134 | + return False |
| 135 | + |
| 136 | + |
| 137 | +def ensure_r_available(): |
| 138 | + """Make sure R is present, installing it (or asking to) only when it |
| 139 | + actually makes sense to do so.""" |
| 140 | + if check_r_installed(): |
| 141 | + print("No R installation needed.") |
| 142 | + return |
| 143 | + |
| 144 | + if _running_noninteractively(): |
| 145 | + if os.environ.get("LEARNINGMACHINE_AUTO_INSTALL_R") == "1": |
| 146 | + print("Non-interactive environment: auto-installing R " |
| 147 | + "(LEARNINGMACHINE_AUTO_INSTALL_R=1 was set).") |
| 148 | + install_r() |
| 149 | + else: |
| 150 | + print( |
| 151 | + "R is not installed and this looks like a non-interactive " |
| 152 | + "environment (CI or no TTY), so setup.py will NOT prompt " |
| 153 | + "for input and will NOT attempt to install R automatically.\n" |
| 154 | + "Set the environment variable LEARNINGMACHINE_AUTO_INSTALL_R=1 " |
| 155 | + "before running setup.py if you want it to try installing R, " |
| 156 | + "or install R yourself first: https://cloud.r-project.org/" |
| 157 | + ) |
| 158 | + return |
| 159 | + |
| 160 | + # Interactive session with a real human at a terminal. |
| 161 | + try: |
| 162 | + install_r_prompt = int(input("Try installing R? 1-yes, 2-no: ")) |
| 163 | + except (EOFError, ValueError): |
| 164 | + install_r_prompt = 2 |
| 165 | + |
| 166 | + if install_r_prompt == 1: |
129 | 167 | print("Installing R...") |
130 | 168 | install_r() |
131 | 169 | else: |
132 | | - raise ValueError('Try installing R manually first.') |
133 | | -else: |
134 | | - print("No R installation needed.") |
| 170 | + print( |
| 171 | + "Skipping R installation. Install R manually first: " |
| 172 | + "https://cloud.r-project.org/" |
| 173 | + ) |
135 | 174 |
|
136 | | -install_packages() |
137 | 175 |
|
138 | | -subprocess.run(["pip", "install", "rpy2"]) |
| 176 | +# Building an sdist/wheel (e.g. in CI to publish a release) never needs R |
| 177 | +# or rpy2 on the build machine -- those are runtime dependencies for |
| 178 | +# whoever installs the package. Only run the R setup dance for actual |
| 179 | +# install-type invocations. |
| 180 | +if not _is_packaging_only_invocation(): |
| 181 | + ensure_r_available() |
| 182 | + if check_r_installed(): |
| 183 | + install_packages() |
| 184 | + subprocess.run([sys.executable, "-m", "pip", "install", "rpy2"]) |
| 185 | +else: |
| 186 | + print(f"Packaging-only invocation ({' '.join(sys.argv[1:])}); " |
| 187 | + "skipping R/rpy2 setup.") |
139 | 188 |
|
140 | 189 | from setuptools import setup, find_packages |
141 | 190 | from codecs import open |
142 | 191 | from os import path |
143 | 192 |
|
144 | 193 | # 4 - Package setup ----------------------------------------------- |
145 | | - |
| 194 | + |
146 | 195 | """The setup script.""" |
147 | 196 |
|
148 | 197 | setup( |
|
0 commit comments