wip

Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/examples/pip_parse/.bazelversion b/examples/pip_parse/.bazelversion
new file mode 100644
index 0000000..af8c8ec
--- /dev/null
+++ b/examples/pip_parse/.bazelversion
@@ -0,0 +1 @@
+4.2.2
diff --git a/examples/pip_parse/BUILD b/examples/pip_parse/BUILD
index 92b59ae..4ae065a 100644
--- a/examples/pip_parse/BUILD
+++ b/examples/pip_parse/BUILD
@@ -4,6 +4,7 @@
     "dist_info_requirement",
     "entry_point",
 )
+load("@rules_python//python/pip_install:pip_repository.bzl", "wheel")
 load("@rules_python//python:defs.bzl", "py_binary", "py_test")
 load("@rules_python//python:pip.bzl", "compile_pip_requirements")
 
@@ -79,3 +80,56 @@
     },
     deps = ["@rules_python//python/runfiles"],
 )
+
+wheel(
+    name = "numpy",
+    src = "@numpy_wheel_source_distribution//file",
+)
+
+# wheel(
+#     name = "pybind11",
+#     src = "@pybind11_wheel_source_distribution//file",
+# )
+
+# wheel(
+#     name = "pythran",
+#     src = "@pythran_wheel_source_distribution//file",
+# )
+
+# wheel(
+#     name = "gast",
+#     src = "@gast_wheel_source_distribution//file",
+# )
+
+# wheel(
+#     name = "beniget",
+#     src = "@beniget_wheel_source_distribution//file",
+# )
+
+# wheel(
+#     name = "ply",
+#     src = "@ply_wheel_source_distribution//file",
+# )
+
+# wheel(
+#     name = "scipy",
+#     src = "@scipy_wheel_source_distribution//file",
+#     deps = [
+#         ":numpy",
+#         ":pybind11",
+#         ":pythran",
+#         ":gast",
+#         ":beniget",
+#         ":ply",
+#     ],
+# )
+
+wheel(
+    name = "boto3",
+    src = "@boto3_wheel_source_distribution//file",
+)
+
+wheel(
+    name = "django",
+    src = "@django_wheel_source_distribution//file",
+)
diff --git a/examples/pip_parse/WORKSPACE b/examples/pip_parse/WORKSPACE
index e96db9f..b706fd6 100644
--- a/examples/pip_parse/WORKSPACE
+++ b/examples/pip_parse/WORKSPACE
@@ -1,18 +1,37 @@
 workspace(name = "rules_python_pip_parse_example")
 
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
+
+http_archive(
+    name = "bazel_skylib",
+    sha256 = "f7be3474d42aae265405a592bb7da8e171919d74c16f082a5457840f06054728",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.2.1/bazel-skylib-1.2.1.tar.gz",
+        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.2.1/bazel-skylib-1.2.1.tar.gz",
+    ],
+)
+
+load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
+
+bazel_skylib_workspace()
+
 local_repository(
     name = "rules_python",
     path = "../..",
 )
 
+load("@rules_python//python/pip_install:repositories.bzl", "pip_install_dependencies")
+
+pip_install_dependencies()
+
 load("@rules_python//python:repositories.bzl", "python_register_toolchains")
 
 python_register_toolchains(
-    name = "python39",
-    python_version = "3.9",
+    name = "python310",
+    python_version = "3.10",
 )
 
-load("@python39//:defs.bzl", "interpreter")
+load("@python310//:defs.bzl", "interpreter")
 load("@rules_python//python:pip.bzl", "pip_parse")
 
 pip_parse(
@@ -48,3 +67,66 @@
 
 # Initialize repositories for all packages in requirements_lock.txt.
 install_deps()
+
+http_file(
+    name = "numpy_wheel_source_distribution",
+    downloaded_file_path = "numpy-1.22.3.zip",
+    sha256 = "dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18",
+    urls = ["https://files.pythonhosted.org/packages/64/4a/b008d1f8a7b9f5206ecf70a53f84e654707e7616a771d84c05151a4713e9/numpy-1.22.3.zip"],
+)
+
+http_file(
+    name = "pybind11_wheel_source_distribution",
+    downloaded_file_path = "pybind11-2.9.2.tar.gz",
+    sha256 = "e5541f8bccf9111d1a94f7897593b55c4cf1a28d5e8cfc8225a855651f011071",
+    urls = ["https://files.pythonhosted.org/packages/cf/6a/a7f2c40fdb43fcf59bc1eebb0a4c4206a99ccddee6391a1168fa6efebce9/pybind11-2.9.2.tar.gz"],
+)
+
+http_file(
+    name = "pythran_wheel_source_distribution",
+    downloaded_file_path = "pythran-0.11.0.tar.gz",
+    sha256 = "0b2cba712e09f7630879dff69f268460bfe34a6d6000451b47d598558a92a875",
+    urls = ["https://files.pythonhosted.org/packages/88/9f/161f08131abf7f23920cee29b691de27f10fd97ac09fb2f3532b3a7f9b96/pythran-0.11.0.tar.gz"],
+)
+
+http_file(
+    name = "gast_wheel_source_distribution",
+    downloaded_file_path = "gast-0.5.3.tar.gz",
+    sha256 = "cfbea25820e653af9c7d1807f659ce0a0a9c64f2439421a7bba4f0983f532dea",
+    urls = ["https://files.pythonhosted.org/packages/48/a3/0bd844c54ae8141642088b7ae09dd38fec2ec7faa9b7d25bb6a23c1f266f/gast-0.5.3.tar.gz"],
+)
+
+http_file(
+    name = "beniget_wheel_source_distribution",
+    downloaded_file_path = "beniget-0.4.1.tar.gz",
+    sha256 = "75554b3b8ad0553ce2f607627dad3d95c60c441189875b98e097528f8e23ac0c",
+    urls = ["https://files.pythonhosted.org/packages/14/e7/50cbac38f77eca8efd39516be6651fdb9f3c4c0fab8cf2cf05f612578737/beniget-0.4.1.tar.gz"],
+)
+
+http_file(
+    name = "ply_wheel_source_distribution",
+    downloaded_file_path = "ply-3.11.tar.gz",
+    sha256 = "00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3",
+    urls = ["https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz"],
+)
+
+http_file(
+    name = "scipy_wheel_source_distribution",
+    downloaded_file_path = "scipy-1.8.0.tar.gz",
+    sha256 = "31d4f2d6b724bc9a98e527b5849b8a7e589bf1ea630c33aa563eda912c9ff0bd",
+    urls = ["https://files.pythonhosted.org/packages/b4/a2/4faa34bf0cdbefd5c706625f1234987795f368eb4e97bde9d6f46860843e/scipy-1.8.0.tar.gz"],
+)
+
+http_file(
+    name = "boto3_wheel_source_distribution",
+    downloaded_file_path = "boto3-1.21.38.tar.gz",
+    sha256 = "62dde36a57697b40b4693e0ad0d39013f1e187e5a3c52fdb50dbe710633061bb",
+    urls = ["https://files.pythonhosted.org/packages/df/89/6b20ad811c1502f9fbb29369556010ef49e830650ad4d6ac257d40134220/boto3-1.21.38.tar.gz"],
+)
+
+http_file(
+    name = "django_wheel_source_distribution",
+    downloaded_file_path = "Django-4.0.4.tar.gz",
+    sha256 = "4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5",
+    urls = ["https://files.pythonhosted.org/packages/d2/95/93d1f75da95624bf89e373d079fa72debf77f9b10acc31998cc52a5ff3f9/Django-4.0.4.tar.gz"],
+)
diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
index e587f34..d5e2a31 100644
--- a/python/pip_install/pip_repository.bzl
+++ b/python/pip_install/pip_repository.bzl
@@ -1,5 +1,15 @@
 ""
 
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
+load(
+    "@bazel_tools//tools/build_defs/cc:action_names.bzl",
+    "ASSEMBLE_ACTION_NAME",
+    "CPP_COMPILE_ACTION_NAME",
+    "CPP_LINK_DYNAMIC_LIBRARY_ACTION_NAME",
+    "CPP_LINK_STATIC_LIBRARY_ACTION_NAME",
+    "C_COMPILE_ACTION_NAME",
+)
 load("//python/pip_install:repositories.bzl", "all_requirements")
 load("//python/pip_install/private:srcs.bzl", "PIP_INSTALL_PY_SRCS")
 
@@ -434,3 +444,209 @@
         data_exclude_glob = data_exclude_glob,
         srcs_exclude_glob = srcs_exclude_glob,
     ))
+
+def _wheel_impl(ctx):
+    tools = [
+        ("CC", C_COMPILE_ACTION_NAME),
+        ("CXX", CPP_COMPILE_ACTION_NAME),
+        ("AS", ASSEMBLE_ACTION_NAME),
+        ("LD", CPP_LINK_DYNAMIC_LIBRARY_ACTION_NAME),
+        ("AR", CPP_LINK_STATIC_LIBRARY_ACTION_NAME),
+    ]
+    flags = [
+        ("LDFLAGS", CPP_LINK_DYNAMIC_LIBRARY_ACTION_NAME, ctx.fragments.cpp.linkopts),
+        ("CFLAGS", C_COMPILE_ACTION_NAME, ctx.fragments.cpp.copts),
+        ("CXXFLAGS", CPP_COMPILE_ACTION_NAME, ctx.fragments.cpp.cxxopts),
+    ]
+    env = _environment_variables(
+        ctx,
+        tools,
+        flags,
+    )
+
+    python_toolchain = ctx.toolchains["@bazel_tools//tools/python:toolchain_type"]
+    runtime = python_toolchain.py3_runtime
+    if not runtime.interpreter:
+        fail("py3_runtime must be set")
+
+    build_deps = ctx.files._build_deps
+    deps = ctx.files.deps
+
+    build_deps_pythonpath = _install_deps(ctx, runtime, env, [], "build_deps", build_deps)
+    deps_pythonpath = _install_deps(ctx, runtime, env, [build_deps_pythonpath], "deps", deps)
+
+    output_name = paths.replace_extension(paths.basename(ctx.file.src.path), ".whl").replace(".tar", "")
+    whl = ctx.actions.declare_file(output_name)
+    progress_message = "Building {}".format(output_name)
+
+    ctx.actions.run_shell(
+        command = """\
+set -o errexit -o nounset -o pipefail
+
+export PYTHONPATH="{pythonpath}"
+
+'{interpreter}' -m pip wheel \
+    --disable-pip-version-check \
+    --no-cache-dir \
+    --no-index \
+    --no-deps \
+    --no-build-isolation \
+    --use-pep517 \
+    --wheel-dir output/ \
+    '{src}'
+
+mv output/*.whl '{whl}'
+""".format(
+            interpreter = runtime.interpreter.path,
+            pythonpath = _construct_pythonpath([build_deps_pythonpath, deps_pythonpath]),
+            src = ctx.file.src.path,
+            whl = whl.path,
+        ),
+        env = env,
+        execution_requirements = {
+            "block-network": "1",
+        },
+        inputs = build_deps + [
+            ctx.file.src,
+            build_deps_pythonpath,
+            deps_pythonpath,
+        ],
+        mnemonic = "BuildWheel",
+        outputs = [whl],
+        progress_message = progress_message,
+        tools = runtime.files,
+    )
+
+    return [DefaultInfo(
+        files = depset([whl]),
+        runfiles = ctx.runfiles([whl]),
+    )]
+
+def _install_deps(ctx, runtime, env, extra_pythonpath, dirname, deps):
+    pythonpath = ctx.actions.declare_directory("{}_{}".format(dirname, ctx.attr.name))
+    commands = [
+        _install_dep_command(runtime.interpreter.path, dep.path, pythonpath.path)
+        for dep in deps
+    ]
+    ctx.actions.run_shell(
+        command = """\
+set -o errexit -o nounset -o pipefail
+
+export PYTHONPATH="{pythonpath}"
+
+{commands}
+""".format(
+            pythonpath = _construct_pythonpath(extra_pythonpath + [pythonpath]),
+            commands = "\n".join(commands),
+        ),
+        env = env,
+        execution_requirements = {
+            "block-network": "1",
+        },
+        inputs = deps + extra_pythonpath,
+        mnemonic = "InstallDeps",
+        outputs = [pythonpath],
+        progress_message = "Installing dependencies",
+        tools = runtime.files,
+    )
+    return pythonpath
+
+def _construct_pythonpath(paths):
+    return ":".join([
+        "$(pwd)/{}".format(p.path)
+        for p in paths
+    ])
+
+def _install_dep_command(interpreter, dep, pythonpath):
+    return """\
+if [[ '{dep}' =~ \\.whl$ ]]; then
+    # '{interpreter}' -m wheel unpack \
+    #     --dest-dir '{pythonpath}' \
+    #     '{dep}'
+    '{interpreter}' <<'EOF'
+import zipfile
+with zipfile.ZipFile('{dep}', 'r') as zf:
+    zf.extractall('{pythonpath}')
+EOF
+else
+    '{interpreter}' -m pip install \
+        --upgrade \
+        --disable-pip-version-check \
+        --no-cache-dir \
+        --no-index \
+        --no-deps \
+        --no-build-isolation \
+        --use-pep517 \
+        --target '{pythonpath}' \
+        '{dep}'
+fi
+""".format(
+            dep = dep,
+            interpreter = interpreter,
+            pythonpath = pythonpath,
+        )
+
+def _environment_variables(ctx, tools, flags):
+    cc_toolchain = find_cpp_toolchain(ctx)
+    feature_configuration = cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+    )
+    env = {}
+    for (var, action) in tools:
+        env[var] = cc_common.get_tool_for_action(
+            feature_configuration = feature_configuration,
+            action_name = action,
+        )
+    for (var, action, user_compile_flags) in flags:
+        compile_variables = cc_common.create_compile_variables(
+            feature_configuration = feature_configuration,
+            cc_toolchain = cc_toolchain,
+            user_compile_flags = user_compile_flags,
+        )
+        env[var] = " ".join(cc_common.get_memory_inefficient_command_line(
+            feature_configuration = feature_configuration,
+            action_name = action,
+            variables = compile_variables,
+        ))
+
+    # TODO(f0rmiga): some libraries will not respect the env vars above, so the
+    # only way to still get them to compile successfuly is to add a PATH.
+    # E.g. compiling numpy calls `as` hardcoded somewhere. Even setting -B in
+    # the CFLAGS, CXXFLAGS and LDFLAGS won't solve fully.
+    # An alternative is to get binutils hermetic and put in the PATH to be found
+    # first.
+    env["PATH"] = "/usr/bin"
+    return env
+
+wheel = rule(
+    _wheel_impl,
+    attrs = {
+        "src": attr.label(
+            allow_single_file = True,
+            doc = "The wheel source distribution file.",
+            mandatory = True,
+        ),
+        "deps": attr.label_list(
+            allow_files = True,
+            doc = "Wheel distributions to be installed before building src.",
+            mandatory = False,
+        ),
+        "_cc_toolchain": attr.label(
+            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
+        ),
+        "_build_deps": attr.label_list(
+            allow_files = True,
+            default = [
+                Label("@pypi__setuptools_whl//file"),
+                Label("@pypi__wheel_whl//file"),
+                Label("@pypi__cython//file"),
+            ],
+        ),
+    },
+    fragments = ["cpp"],
+    toolchains = [
+        "@bazel_tools//tools/cpp:toolchain_type",
+        "@bazel_tools//tools/python:toolchain_type",
+    ],
+)
diff --git a/python/pip_install/repositories.bzl b/python/pip_install/repositories.bzl
index 352f663..049f3f1 100644
--- a/python/pip_install/repositories.bzl
+++ b/python/pip_install/repositories.bzl
@@ -1,6 +1,7 @@
 ""
 
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
 load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
 
 # Avoid a load from @bazel_skylib repository as users don't necessarily have it installed
@@ -42,6 +43,11 @@
         "https://files.pythonhosted.org/packages/27/d6/003e593296a85fd6ed616ed962795b2f87709c3eee2bca4f6d0fe55c6d00/wheel-0.37.1-py2.py3-none-any.whl",
         "4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a",
     ),
+    (
+        "pypi__cython",
+        "https://files.pythonhosted.org/packages/cb/da/54a5d7a7d9afc90036d21f4b58229058270cc14b4c81a86d9b2c77fd072e/Cython-0.29.28.tar.gz",
+        "d6fac2342802c30e51426828fe084ff4deb1b3387367cf98976bb2e64b6f8e45",
+    ),
 ]
 
 _GENERIC_WHEEL = """\
@@ -60,7 +66,7 @@
 """
 
 # Collate all the repository names so they can be easily consumed
-all_requirements = [name for (name, _, _) in _RULE_DEPS]
+all_requirements = [name for (name, url, _) in _RULE_DEPS if url.endswith(".whl")]
 
 def requirement(pkg):
     return "@pypi__" + pkg + "//:lib"
@@ -79,11 +85,30 @@
     versions.check("4.0.0")
 
     for (name, url, sha256) in _RULE_DEPS:
-        maybe(
-            http_archive,
-            name,
-            url = url,
-            sha256 = sha256,
-            type = "zip",
-            build_file_content = _GENERIC_WHEEL,
-        )
+        filename = paths.basename(url)
+
+        if filename.endswith(".whl"):
+            maybe(
+                http_file,
+                name + "_whl",
+                urls = [url],
+                sha256 = sha256,
+                downloaded_file_path = filename,
+            )
+
+            maybe(
+                http_archive,
+                name,
+                urls = [url],
+                sha256 = sha256,
+                type = "zip",
+                build_file_content = _GENERIC_WHEEL,
+            )
+        else:
+            maybe(
+                http_file,
+                name,
+                urls = [url],
+                sha256 = sha256,
+                downloaded_file_path = filename,
+            )