aboutsummaryrefslogtreecommitdiffhomepage
path: root/third_party/py/python_configure.bzl
blob: c453645db5af459ffa563340ea84832affa072e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# -*- Python -*-
"""Repository rule for Python autoconfiguration.

`python_configure` depends on the following environment variables:

  * `NUMPY_INCLUDE_PATH`: Location of Numpy libraries.
  * `PYTHON_BIN_PATH`: location of python binary.
  * `PYTHON_INCLUDE_PATH`: Location of python binaries.
  * `PYTHON_LIB_PATH`: Location of python libraries.
"""

_NUMPY_INCLUDE_PATH = "NUMPY_INCLUDE_PATH"
_PYTHON_BIN_PATH = "PYTHON_BIN_PATH"
_PYTHON_INCLUDE_PATH = "PYTHON_INCLUDE_PATH"
_PYTHON_LIB_PATH = "PYTHON_LIB_PATH"


def _tpl(repository_ctx, tpl, substitutions={}, out=None):
  if not out:
    out = tpl
  repository_ctx.template(
      out,
      Label("//third_party/py:%s.tpl" % tpl),
      substitutions)


def _python_configure_warning(msg):
  """Output warning message during auto configuration."""
  yellow = "\033[1;33m"
  no_color = "\033[0m"
  print("\n%sPython Configuration Warning:%s %s\n" % (yellow, no_color, msg))


def _python_configure_fail(msg):
  """Output failure message when auto configuration fails."""
  red = "\033[0;31m"
  no_color = "\033[0m"
  fail("\n%sPython Configuration Error:%s %s\n" % (red, no_color, msg))


def _get_env_var(repository_ctx, name, default = None, enable_warning = True):
  """Find an environment variable in system path."""
  if name in repository_ctx.os.environ:
    return repository_ctx.os.environ[name]
  if default != None:
    if enable_warning:
      _python_configure_warning(
          "'%s' environment variable is not set, using '%s' as default" % (name, default))
    return default
  _python_configure_fail("'%s' environment variable is not set" % name)


def _is_windows(repository_ctx):
  """Returns true if the host operating system is windows."""
  os_name = repository_ctx.os.name.lower()
  if os_name.find("windows") != -1:
    return True
  return False


def _execute(repository_ctx, cmdline, error_msg=None, error_details=None,
             empty_stdout_fine=False):
  """Executes an arbitrary shell command.

  Args:
    repository_ctx: the repository_ctx object
    cmdline: list of strings, the command to execute
    error_msg: string, a summary of the error if the command fails
    error_details: string, details about the error or steps to fix it
    empty_stdout_fine: bool, if True, an empty stdout result is fine, otherwise
      it's an error
  Return:
    the result of repository_ctx.execute(cmdline)
  """
  result = repository_ctx.execute(cmdline)
  if result.stderr or not (empty_stdout_fine or result.stdout):
    _python_configure_fail(
        "\n".join([
            error_msg.strip() if error_msg else "Repository command failed",
            result.stderr.strip(),
            error_details if error_details else ""]))
  return result


def _symlink_genrule_for_dir(repository_ctx, src_dir, dest_dir, genrule_name):
  """returns a genrule to symlink all files in a directory."""
  # Get the list of files under this directory
  find_result = None
  if _is_windows(repository_ctx):
    find_result = _execute(
        repository_ctx,
        ["cmd.exe", "/c", "dir", src_dir.replace("/", "\\"), "/b", "/s",
         "/a-d"],
        empty_stdout_fine=True)
    # src_files will be used to compute BUILD rules, where path must use
    # forward slashes.
    src_files = find_result.stdout.replace("\\", "/").splitlines()
    # Create a list with the src_dir stripped to use for outputs.
    fwdslashes_src_dir = src_dir.replace("\\", "/")
    dest_files = [e.replace(fwdslashes_src_dir, "") for e in src_files]
  else:
    find_result = _execute(
        repository_ctx, ["find", src_dir, "-follow", "-type", "f"],
        empty_stdout_fine=True)
    # Create a list with the src_dir stripped to use for outputs.
    dest_files = find_result.stdout.replace(src_dir, '').splitlines()
    src_files = find_result.stdout.splitlines()
  command = []
  command_windows = []
  outs = []
  outs_windows = []
  for i in range(len(dest_files)):
    if dest_files[i] != "":
      command.append('ln -s ' + src_files[i] + ' $(@D)/' +
                     dest_dir + dest_files[i])
      # ln -sf is actually implemented as copying in msys since creating
      # symbolic links is privileged on Windows. But copying is too slow, so
      # invoke mklink to create junctions on Windows.
      command_windows.append('mklink /J ' + src_files[i] + ' $(@D)/' +
                             dest_dir + dest_files[i])
      outs.append('      "' + dest_dir + dest_files[i] + '",')
      outs_windows.append('      "' + dest_dir + '_windows' +
                          dest_files[i] + '",')
  genrule = _genrule(src_dir, genrule_name, ' && '.join(command),
                     '\n'.join(outs))
  genrule_windows = _genrule(src_dir, genrule_name + '_windows',
                             "cmd /c \"" + ' && '.join(command_windows) + "\"",
                             '\n'.join(outs_windows))
  return genrule + '\n' + genrule_windows


def _genrule(src_dir, genrule_name, command, outs):
  """Returns a string with a genrule.

  Genrule executes the given command and produces the given outputs.
  """
  return (
      'genrule(\n' +
      '    name = "' +
      genrule_name + '",\n' +
      '    outs = [\n' +
      outs +
      '    ],\n' +
      '    cmd = """\n' +
      command +
      '    """,\n' +
      '    visibility = ["//visibility:private"],' +
      ')\n'
  )


def _check_python_lib(repository_ctx, python_lib):
  """Checks the python lib path."""
  cmd = 'test -d "%s" -a -x "%s"' % (python_lib, python_lib)
  result = repository_ctx.execute(["bash", "-c", cmd])
  if result.return_code == 1:
    _python_configure_fail("Invalid python library path:  %s" % python_lib)


def _check_python_bin(repository_ctx, python_bin):
  """Checks the python bin path."""
  cmd =  '[[ -x "%s" ]] && [[ ! -d "%s" ]]' % (python_bin, python_bin)
  result = repository_ctx.execute(["bash", "-c", cmd])
  if result.return_code == 1:
    _python_configure_fail(
        "PYTHON_BIN_PATH is not executable.  Is it the python binary?")


def _get_python_include(repository_ctx, python_bin):
  """Gets the python include path."""
  result = _execute(repository_ctx,
                    [python_bin, "-c",
                     'from __future__ import print_function;' +
                     'from distutils import sysconfig;' +
                     'print(sysconfig.get_python_inc())'],
                    error_msg="Problem getting python include path.",
                    error_details=("Is the Python binary path set up right? " +
                                   "(See ./configure or BAZEL_BIN_PATH.) " +
                                   "Is distutils installed?"))
  return result.stdout.splitlines()[0]


def _get_numpy_include(repository_ctx, python_bin):
  """Gets the numpy include path."""
  return _execute(repository_ctx,
                  [python_bin, "-c",
                   'from __future__ import print_function;' +
                   'import numpy;' +
                   ' print(numpy.get_include());'],
                  error_msg="Problem getting numpy include path.",
                  error_details="Is numpy installed?").stdout.splitlines()[0]


def _create_local_python_repository(repository_ctx):
  """Creates the repository containing files set up to build with Python."""
  python_include = None
  numpy_include = None
  empty_config = False
  # If local checks were requested, the python and numpy include will be auto
  # detected on the host config (using _PYTHON_BIN_PATH).
  if repository_ctx.attr.local_checks:
    python_bin = _get_env_var(repository_ctx, _PYTHON_BIN_PATH)
    _check_python_bin(repository_ctx, python_bin)
    python_lib = _get_env_var(repository_ctx, _PYTHON_LIB_PATH, '')
    if python_lib == '':
      # If we could not find the python lib we will create an empty config that
      # will allow non-compilation targets to build correctly (e.g., smoke
      # tests).
      empty_config = True
      _python_configure_warning('PYTHON_LIB_PATH was not set;' +
                                ' python setup cannot complete successfully.' +
                                ' Please run ./configure.')
    else:
      _check_python_lib(repository_ctx, python_lib)
      python_include = _get_python_include(repository_ctx, python_bin)
      numpy_include = _get_numpy_include(repository_ctx, python_bin) + '/numpy'
  else:
    # Otherwise, we assume user provides all paths (via ENV or attrs)
    python_include = _get_env_var(repository_ctx, _PYTHON_INCLUDE_PATH,
                                  repository_ctx.attr.python_include)
    numpy_include = _get_env_var(repository_ctx, _NUMPY_INCLUDE_PATH,
                                 repository_ctx.attr.numpy_include) + '/numpy'
  if empty_config:
    _tpl(repository_ctx, "BUILD", {
        "%{PYTHON_INCLUDE_GENRULE}": ('filegroup(\n' +
                                      '    name = "python_include",\n' +
                                      '    srcs = [],\n' +
                                      ')\n'),
        "%{NUMPY_INCLUDE_GENRULE}": ('filegroup(\n' +
                                      '    name = "numpy_include",\n' +
                                      '    srcs = [],\n' +
                                      ')\n'),
    })
  else:
    python_include_rule = _symlink_genrule_for_dir(
        repository_ctx, python_include, 'python_include', 'python_include')
    numpy_include_rule = _symlink_genrule_for_dir(
        repository_ctx, numpy_include, 'numpy_include/numpy', 'numpy_include')
    _tpl(repository_ctx, "BUILD", {
        "%{PYTHON_INCLUDE_GENRULE}": python_include_rule,
        "%{NUMPY_INCLUDE_GENRULE}": numpy_include_rule,
    })


def _create_remote_python_repository(repository_ctx):
  """Creates pointers to a remotely configured repo set up to build with Python.
  """
  _tpl(repository_ctx, "remote.BUILD", {
      "%{REMOTE_PYTHON_REPO}": repository_ctx.attr.remote_config_repo,
  }, "BUILD")


def _python_autoconf_impl(repository_ctx):
  """Implementation of the python_autoconf repository rule."""
  if repository_ctx.attr.remote_config_repo != "":
    _create_remote_python_repository(repository_ctx)
  else:
    _create_local_python_repository(repository_ctx)


python_configure = repository_rule(
    implementation = _python_autoconf_impl,
    attrs = {
        "local_checks": attr.bool(mandatory = False, default = True),
        "python_include": attr.string(mandatory = False),
        "numpy_include": attr.string(mandatory = False),
        "remote_config_repo": attr.string(mandatory = False, default =""),
    },
    environ = [
        _PYTHON_BIN_PATH,
        _PYTHON_INCLUDE_PATH,
        _PYTHON_LIB_PATH,
        _NUMPY_INCLUDE_PATH,
    ],
)
"""Detects and configures the local Python.

Add the following to your WORKSPACE FILE:

```python
python_configure(name = "local_config_python")
```

Args:
  name: A unique name for this workspace rule.
"""