From 40dd9d4a44cdffac817d369922afc85702744573 Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Thu, 9 May 2024 19:07:04 -0400 Subject: [PATCH] plugin release notes --- docs/notes/2.22.x.md | 13 ++ .../pants/backend/adhoc/adhoc_tool_test.py | 32 ++++ .../shell/util_rules/shell_command_test.py | 35 ++++ src/python/pants/core/register.py | 2 + .../core/util_rules/adhoc_process_support.py | 12 +- .../pants/core/util_rules/environments.py | 156 ++++++++++++++++-- .../pants/core/util_rules/system_binaries.py | 15 +- src/python/pants/engine/environment.py | 12 +- src/python/pants/engine/process_test.py | 40 +++-- src/rust/engine/Cargo.lock | 8 +- 10 files changed, 289 insertions(+), 36 deletions(-) diff --git a/docs/notes/2.22.x.md b/docs/notes/2.22.x.md index 5633503771e..58b25e6aadc 100644 --- a/docs/notes/2.22.x.md +++ b/docs/notes/2.22.x.md @@ -13,6 +13,7 @@ We offer [formal sponsorship tiers for companies](https://www.pantsbuild.org/spo ### Highlights - A new implementation of the options system. +- In-workspace execution of processes via `workspace_environment` target type. ### New options system @@ -22,6 +23,16 @@ The two systems are expected to be more-or-less functionally identical. We plan If you encounter such discrepancies, and you can't resolve them easily, please [reach out to us on Slack or file an issue](https://www.pantsbuild.org/community/getting-help). +### Environments: In-Workspace Execution + +Pants now supports executing processes locally within the repository itself via the new "workspace" environment which is configured by the new `workspace_environment` target type. The primary motivation for this feature is to better support integration with third-party build orchestration tools (e.g., Bazel) which may not operate properly when not invoked in the repository (including in some cases signifcant performance penalties). + +There is a significant trade-off though: Pants cannot reasonbly guarantee that build processes are reproducible if they run in the workspace +environment. Thus, Pants puts that burden on you, the Pants user, to guarantee that any process executed in the workspace environment is reproducible +based solely on inputs in the repository. If a process is not reproducible, then unknown side effects may occur. + +**Thus, this feature is inherently UNSAFE.** + ### Backends #### JVM @@ -58,6 +69,8 @@ Setting [the `orphan_files_behaviour = "ignore"` option](https://www.pantsbuild. ### Plugin API changes +The process execution intrinsic rule in Rust now contains support for "workspace" execution. This is local execution from within the repository itself without using an execution sandbox. `ProcessExecutionEnvironment`'s constructor has a new `execute_in_workspace` parameter which enables workspace execution. + ## Full Changelog For the full changelog, see the individual GitHub Releases for this series: https://github.com/pantsbuild/pants/releases diff --git a/src/python/pants/backend/adhoc/adhoc_tool_test.py b/src/python/pants/backend/adhoc/adhoc_tool_test.py index 7b1620c8fda..ebe54b7329f 100644 --- a/src/python/pants/backend/adhoc/adhoc_tool_test.py +++ b/src/python/pants/backend/adhoc/adhoc_tool_test.py @@ -3,6 +3,7 @@ from __future__ import annotations +from pathlib import Path from textwrap import dedent import pytest @@ -17,6 +18,7 @@ from pants.core.target_types import rules as core_target_type_rules from pants.core.util_rules import archive, source_files from pants.core.util_rules.adhoc_process_support import AdhocProcessRequest +from pants.core.util_rules.environments import LocalWorkspaceEnvironmentTarget from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest from pants.engine.addresses import Address from pants.engine.fs import EMPTY_SNAPSHOT, DigestContents @@ -52,6 +54,7 @@ def rule_runner() -> PythonRuleRunner: ArchiveTarget, FilesGeneratorTarget, PythonSourceTarget, + LocalWorkspaceEnvironmentTarget, ], ) rule_runner.set_options([], env_inherit={"PATH"}) @@ -302,3 +305,32 @@ def test_execution_dependencies_and_runnable_dependencies(rule_runner: PythonRul Address("b", target_name="deps"), expected_contents={"b/stdout": file_contents}, ) + + +def test_adhoc_tool_with_workspace_execution(rule_runner: PythonRuleRunner) -> None: + rule_runner.write_files( + { + "BUILD": dedent( + """ + system_binary(name="bash", binary_name="bash") + adhoc_tool( + name="make-file", + runnable=":bash", + args=["-c", "echo 'workspace' > ./foo.txt"], + environment="workspace", + stderr="stderr", + ) + workspace_environment(name="workspace") + """ + ) + } + ) + rule_runner.set_options( + ["--environments-preview-names={'workspace': '//:workspace'}"], env_inherit={"PATH"} + ) + + assert_adhoc_tool_result(rule_runner, Address("", target_name="make-file"), {"stderr": ""}) + + workspace_output_path = Path(rule_runner.build_root).joinpath("foo.txt") + assert workspace_output_path.exists() + assert workspace_output_path.read_text().strip() == "workspace" diff --git a/src/python/pants/backend/shell/util_rules/shell_command_test.py b/src/python/pants/backend/shell/util_rules/shell_command_test.py index 87a9d5ecf73..af3ea942f05 100644 --- a/src/python/pants/backend/shell/util_rules/shell_command_test.py +++ b/src/python/pants/backend/shell/util_rules/shell_command_test.py @@ -5,6 +5,7 @@ import logging import shlex +from pathlib import Path from textwrap import dedent import pytest @@ -26,6 +27,7 @@ from pants.core.target_types import rules as core_target_type_rules from pants.core.util_rules import archive, source_files from pants.core.util_rules.adhoc_process_support import AdhocProcessRequest +from pants.core.util_rules.environments import LocalWorkspaceEnvironmentTarget from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest from pants.engine.addresses import Address from pants.engine.environment import EnvironmentName @@ -65,6 +67,7 @@ def rule_runner() -> RuleRunner: ShellSourcesGeneratorTarget, ArchiveTarget, FilesGeneratorTarget, + LocalWorkspaceEnvironmentTarget, ], ) rule_runner.set_options([], env_inherit={"PATH"}) @@ -837,3 +840,35 @@ def test_working_directory_special_values( Address("src", target_name="workdir"), expected_contents={"out.log": f"{expected_dir}\n"}, ) + + +def test_shell_command_with_workspace_execution(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "BUILD": dedent( + """ + shell_command( + name="make-file", + command="echo workspace > foo.txt && echo sandbox > {chroot}/out.log", + output_files=["out.log"], + environment="workspace", + ) + workspace_environment(name="workspace") + """ + ) + } + ) + rule_runner.set_options( + ["--environments-preview-names={'workspace': '//:workspace'}", "--no-local-cache"], + env_inherit={"PATH"}, + ) + + print(f"build root = {rule_runner.build_root}") + assert_shell_command_result( + rule_runner, + Address("", target_name="make-file"), + expected_contents={"out.log": "sandbox\n"}, + ) + workspace_output_path = Path(rule_runner.build_root).joinpath("foo.txt") + assert workspace_output_path.exists() + assert workspace_output_path.read_text().strip() == "workspace" diff --git a/src/python/pants/core/register.py b/src/python/pants/core/register.py index 0c0e19bde4e..69f322e0d7c 100644 --- a/src/python/pants/core/register.py +++ b/src/python/pants/core/register.py @@ -54,6 +54,7 @@ from pants.core.util_rules.environments import ( DockerEnvironmentTarget, LocalEnvironmentTarget, + LocalWorkspaceEnvironmentTarget, RemoteEnvironmentTarget, ) from pants.core.util_rules.wrap_source import wrap_source_rule_and_target @@ -113,6 +114,7 @@ def target_types(): FileTarget, GenericTarget, LocalEnvironmentTarget, + LocalWorkspaceEnvironmentTarget, LockfilesGeneratorTarget, LockfileTarget, RelocatedFiles, diff --git a/src/python/pants/core/util_rules/adhoc_process_support.py b/src/python/pants/core/util_rules/adhoc_process_support.py index f5ed03fdffd..303457fb9e5 100644 --- a/src/python/pants/core/util_rules/adhoc_process_support.py +++ b/src/python/pants/core/util_rules/adhoc_process_support.py @@ -30,7 +30,13 @@ Snapshot, ) from pants.engine.internals.native_engine import AddressInput, RemovePrefix -from pants.engine.process import FallibleProcessResult, Process, ProcessResult, ProductDescription +from pants.engine.process import ( + FallibleProcessResult, + Process, + ProcessCacheScope, + ProcessResult, + ProductDescription, +) from pants.engine.rules import Get, MultiGet, collect_rules, rule from pants.engine.target import ( FieldSetsPerTarget, @@ -581,6 +587,10 @@ async def prepare_adhoc_process( working_directory=working_directory, append_only_caches=append_only_caches, immutable_input_digests=immutable_input_digests, + # TODO: This is necessary for tests of `adhoc_tool` and `shell_command` with + # workspace execution to pass repeatedly in local Pants development. + # We need a viable solution instead of this hack. + cache_scope=ProcessCacheScope.PER_SESSION, ) return _output_at_build_root(proc, bash) diff --git a/src/python/pants/core/util_rules/environments.py b/src/python/pants/core/util_rules/environments.py index 35208016609..ac4509cafd6 100644 --- a/src/python/pants/core/util_rules/environments.py +++ b/src/python/pants/core/util_rules/environments.py @@ -12,8 +12,11 @@ from pants.build_graph.address import Address, AddressInput from pants.engine.engine_aware import EngineAwareParameter -from pants.engine.environment import LOCAL_ENVIRONMENT_MATCHER, LOCAL_WORKSPACE_ENV_NAME +from pants.engine.environment import LOCAL_ENVIRONMENT_MATCHER, LOCAL_WORKSPACE_ENVIRONMENT_MATCHER from pants.engine.environment import ChosenLocalEnvironmentName as ChosenLocalEnvironmentName +from pants.engine.environment import ( + ChosenLocalWorkspaceEnvironmentName as ChosenLocalWorkspaceEnvironmentName, +) from pants.engine.environment import EnvironmentName as EnvironmentName from pants.engine.internals.docker import DockerResolveImageRequest, DockerResolveImageResult from pants.engine.internals.graph import WrappedTargetForBootstrap @@ -175,6 +178,62 @@ class LocalEnvironmentTarget(Target): ) +class LocalWorkspaceFallbackEnvironmentField(FallbackEnvironmentField): + help = help_text( + f""" + The environment to fallback to when this local workspace environment cannot be used because the + field `{CompatiblePlatformsField.alias}` is not compatible with the local host. + + Must be an environment name from the option `[environments-preview].names`, the + special string `{LOCAL_WORKSPACE_ENVIRONMENT_MATCHER}` to use the relevant local environment, or the + Python value `None` to error when this specific local environment cannot be used. + + Tip: when targeting Linux, it can be particularly helpful to fallback to a + `docker_environment` or `remote_environment` target. That allows you to prefer using the + local host when possible, which often has less overhead (particularly compared to Docker). + If the local host is not compatible, then Pants will use Docker or remote execution to + still run in a similar environment. + """ + ) + + +class LocalWorkspaceEnvironmentTarget(Target): + alias = "workspace_environment" + core_fields = ( + *COMMON_TARGET_FIELDS, + CompatiblePlatformsField, + LocalWorkspaceFallbackEnvironmentField, + ) + help = help_text( + """ + Configuration of a "workspace" execution environment for specific platforms. + + A "workspace" environment is a local environment which executes build processes within + the repository and not in the usual execution sandbox. This is useful when interacting with + third-party build orchestration tools which may not run correctly when run from within the Pants + execution sandbox. + + SAFETY: Workspace environments are inherently UNSAFE since Pants cannot guarantee that any process invoked + as a workspace environment is in fact reproducible. By using this environment, you forgo the + "hermetic" nature of the Pants execution model and must guarantee to Pants that the invoked process + is repeatable and does not have side effects. + + Environment configuration includes the platforms the environment is compatible with, and + optionally a fallback environment, along with environment-aware options (such as + environment variables and search paths) used by Pants to execute processes in this + environment. + + To use this environment, map this target's address with a memorable name in + `[environments-preview].names`. You can then consume this environment by specifying the name in + the `environment` field defined on other targets. + + Only one `workspace_environment` may be defined in `[environments-preview].names` per platform, and + when `{LOCAL_WORKSPACE_ENVIRONMENT_MATCHER}` is specified as the environment, the + `workspace_environment` that matches the current platform (if defined) will be selected. + """ + ) + + class DockerImageField(StringField): alias = "image" required = True @@ -514,6 +573,9 @@ def executable_search_path_cache_scope( else ProcessCacheScope.PER_RESTART_SUCCESSFUL ) + def is_workspace_environment(self): + return self.val and self.val.has_field(LocalWorkspaceFallbackEnvironmentField) + def _compute_env_field(field_set: FieldSet) -> EnvironmentField: for attr in dir(field_set): @@ -665,6 +727,75 @@ async def determine_local_environment( ) +# TODO: Consider refactoring with determine_local_workspace above. +@rule +async def determine_local_workspace_environment( + all_environment_targets: AllEnvironmentTargets, +) -> ChosenLocalWorkspaceEnvironmentName: + platform = Platform.create_for_localhost() + compatible_name_and_targets = [ + (name, tgt) + for name, tgt in all_environment_targets.items() + if tgt.has_field(CompatiblePlatformsField) + and tgt.has_field(LocalWorkspaceFallbackEnvironmentField) + and platform.value in tgt[CompatiblePlatformsField].value + ] + + if not compatible_name_and_targets: + # Raise an exception since, unlike with `local_environment`, a `workspace_environment` + # cannot be configured via global options. + raise AmbiguousEnvironmentError( + softwrap( + f""" + A target requested a compatible workspace environment via the + `{LOCAL_WORKSPACE_ENVIRONMENT_MATCHER}` special environment name. No `workspace_environment` + target exists, however, to satisfy that request. + + Unlike local environmnts, with workspace environments, at least one `workspace_environment` + target must exist and be named in the `[environments-preview.names]` option. + """ + ) + ) + + if len(compatible_name_and_targets) == 1: + result_name, _tgt = compatible_name_and_targets[0] + return ChosenLocalWorkspaceEnvironmentName(EnvironmentName(result_name)) + + raise AmbiguousEnvironmentError( + softwrap( + f""" + Multiple `workspace_environment` targets from `[environments-preview].names` + are compatible with the current platform `{platform.value}`, so it is ambiguous + which to use: + {sorted(tgt.address.spec for _name, tgt in compatible_name_and_targets)} + + To fix, either adjust the `{CompatiblePlatformsField.alias}` field from those + targets so that only one includes the value `{platform.value}`, or change + `[environments-preview].names` so that it does not define some of those targets. + + It is often useful to still keep the same `workspace_environment` target definitions in + BUILD files; instead, do not give a name to each of them in + `[environments-preview].names` to avoid ambiguity. Then, you can override which target + a particular name points to by overriding `[environments-preview].names`. For example, + you could set this in `pants.toml`: + + [environments-preview.names] + linux = "//:linux_env" + macos = "//:macos_local_env" + + Then, for CI, override what the name `macos` points to by setting this in + `pants.ci.toml`: + + [environments-preview.names.add] + macos = "//:macos_ci_env" + + Locally, you can override `[environments-preview].names` like this by using a + `.pants.rc` file, for example. + """ + ) + ) + + @rule async def resolve_single_environment_name( request: SingleEnvironmentNameRequest, @@ -708,8 +839,9 @@ async def resolve_environment_name( if request.raw_value == LOCAL_ENVIRONMENT_MATCHER: local_env_name = await Get(ChosenLocalEnvironmentName) return local_env_name.val - if request.raw_value == LOCAL_WORKSPACE_ENV_NAME: - return EnvironmentName(LOCAL_WORKSPACE_ENV_NAME) + if request.raw_value == LOCAL_WORKSPACE_ENVIRONMENT_MATCHER: + local_workspace_env_name = await Get(ChosenLocalWorkspaceEnvironmentName) + return local_workspace_env_name.val if request.raw_value not in environments_subsystem.names: raise UnrecognizedEnvironmentError( softwrap( @@ -816,8 +948,6 @@ async def get_target_for_environment_name( ) -> EnvironmentTarget: if env_name.val is None: return EnvironmentTarget(None, None) - if env_name.val == LOCAL_WORKSPACE_ENV_NAME: - return EnvironmentTarget(LOCAL_WORKSPACE_ENV_NAME, None) if env_name.val not in environments_subsystem.names: raise AssertionError( softwrap( @@ -899,19 +1029,10 @@ async def extract_process_config_from_environment( global_options: GlobalOptions, environments_subsystem: EnvironmentsSubsystem, ) -> ProcessExecutionEnvironment: - if tgt.name == LOCAL_WORKSPACE_ENV_NAME: - return ProcessExecutionEnvironment( - environment_name=LOCAL_WORKSPACE_ENV_NAME, - platform=platform.value, - remote_execution=False, - remote_execution_extra_platform_properties=[], - docker_image=None, - execute_in_workspace=True, - ) - docker_image = None remote_execution = False raw_remote_execution_extra_platform_properties: tuple[str, ...] = () + execute_in_workspace = False if environments_subsystem.remote_execution_used_globally(global_options): remote_execution = True @@ -947,6 +1068,8 @@ async def extract_process_config_from_environment( ) ) + execute_in_workspace = tgt.val.has_field(LocalWorkspaceFallbackEnvironmentField) + return ProcessExecutionEnvironment( environment_name=tgt.name, platform=platform.value, @@ -956,7 +1079,7 @@ async def extract_process_config_from_environment( tuple(pair.split("=", maxsplit=1)) # type: ignore[misc] for pair in raw_remote_execution_extra_platform_properties ], - execute_in_workspace=False, + execute_in_workspace=execute_in_workspace, ) @@ -1061,6 +1184,7 @@ class OptionField(field_type, _EnvironmentSensitiveOptionFieldMixin): # type: i return [ LocalEnvironmentTarget.register_plugin_field(OptionField), + LocalWorkspaceEnvironmentTarget.register_plugin_field(OptionField), DockerEnvironmentTarget.register_plugin_field(OptionField), RemoteEnvironmentTarget.register_plugin_field(OptionField), ] diff --git a/src/python/pants/core/util_rules/system_binaries.py b/src/python/pants/core/util_rules/system_binaries.py index 71f8ef4b990..68238b48fa5 100644 --- a/src/python/pants/core/util_rules/system_binaries.py +++ b/src/python/pants/core/util_rules/system_binaries.py @@ -506,7 +506,16 @@ async def find_binary( bash = await Get(BashBinary) shebang = f"#!{bash.path}" - script_path = "./find_binary.sh" + # HACK: For workspace environments, the `find_binary.sh` will be mounted in the "chroot" directory + # which is not the current directory (since the process will execute in the workspace). Thus, + # adjust the path used as argv[0] to find the script. + script_name = "find_binary.sh" + script_exec_path = ( + f"./{script_name}" + if not env_target.is_workspace_environment() + else f"{{chroot}}/{script_name}" + ) + script_header = dedent( f"""\ {shebang} @@ -538,7 +547,7 @@ async def find_binary( script_content = script_header + script_body script_digest = await Get( Digest, - CreateDigest([FileContent(script_path, script_content.encode(), is_executable=True)]), + CreateDigest([FileContent(script_name, script_content.encode(), is_executable=True)]), ) # Some subtle notes about executing this script: @@ -552,7 +561,7 @@ async def find_binary( description=f"Searching for `{request.binary_name}` on PATH={search_path}", level=LogLevel.DEBUG, input_digest=script_digest, - argv=[script_path, request.binary_name], + argv=[script_exec_path, request.binary_name], env={"PATH": search_path}, cache_scope=env_target.executable_search_path_cache_scope(), ), diff --git a/src/python/pants/engine/environment.py b/src/python/pants/engine/environment.py index 0464c7bf0b5..c3f8691aeb5 100644 --- a/src/python/pants/engine/environment.py +++ b/src/python/pants/engine/environment.py @@ -9,8 +9,8 @@ # Reserved sentinnel value directing Pants to find applicable local environment. LOCAL_ENVIRONMENT_MATCHER = "__local__" -# Reserved sentinnel value representing execution within the workspace and not local sandbox. -LOCAL_WORKSPACE_ENV_NAME = "__local_workspace__" +# Reserved sentinnel value directing Pants to find applicable workspace environment. +LOCAL_WORKSPACE_ENVIRONMENT_MATCHER = "__local_workspace__" @dataclass(frozen=True) @@ -34,3 +34,11 @@ class ChosenLocalEnvironmentName: """Which environment name from `[environments-preview].names` that __local__ resolves to.""" val: EnvironmentName + + +@dataclass(frozen=True) +class ChosenLocalWorkspaceEnvironmentName: + """Which environment name from `[environments-preview].names` that __local_workspace__ resolves + to.""" + + val: EnvironmentName diff --git a/src/python/pants/engine/process_test.py b/src/python/pants/engine/process_test.py index 75480684f5a..a50d925beee 100644 --- a/src/python/pants/engine/process_test.py +++ b/src/python/pants/engine/process_test.py @@ -8,7 +8,8 @@ import pytest -from pants.engine.environment import LOCAL_WORKSPACE_ENV_NAME, EnvironmentName +from pants.core.util_rules.environments import LocalWorkspaceEnvironmentTarget +from pants.engine.environment import EnvironmentName from pants.engine.fs import ( EMPTY_DIGEST, CreateDigest, @@ -309,8 +310,27 @@ def test_interactive_process_inputs(rule_runner: RuleRunner, run_in_workspace: b } -def test_workspace_process_basic(rule_runner) -> None: - rule_runner = new_rule_runner(inherent_environment=EnvironmentName(LOCAL_WORKSPACE_ENV_NAME)) +def test_workspace_execution_support() -> None: + rule_runner = RuleRunner( + rules=[ + QueryRule(ProcessResult, [Process]), + QueryRule(FallibleProcessResult, [Process]), + QueryRule(InteractiveProcessResult, [InteractiveProcess]), + QueryRule(DigestEntries, [Digest]), + QueryRule(Platform, []), + ], + target_types=[LocalWorkspaceEnvironmentTarget], + inherent_environment=EnvironmentName("workspace"), + ) + rule_runner.write_files( + { + "BUILD": "workspace_environment(name='workspace')", + } + ) + rule_runner.set_options( + ["--environments-preview-names={'workspace': '//:workspace'}"], env_inherit={"PATH"} + ) + build_root = Path(rule_runner.build_root) # Check that a custom exit code is returned as expected. @@ -351,8 +371,8 @@ def test_workspace_process_basic(rule_runner) -> None: description="a workspace process", cache_scope=ProcessCacheScope.PER_SESSION, # necessary to ensure result not cached from prior test runs ) - result = rule_runner.request(ProcessResult, [process]) - lines = result.stdout.decode().splitlines() + result2 = rule_runner.request(ProcessResult, [process]) + lines = result2.stdout.decode().splitlines() assert lines == [ "from-digest", rule_runner.build_root, @@ -369,7 +389,7 @@ def test_workspace_process_basic(rule_runner) -> None: working_directory="subdir", cache_scope=ProcessCacheScope.PER_SESSION, # necessary to ensure result not cached from prior test runs ) - result = rule_runner.request(ProcessResult, [process]) + _ = rule_runner.request(ProcessResult, [process]) assert (subdir / "file-in-subdir").exists() # Test output capture correctly captures from the sandbox and not the workspace. @@ -386,8 +406,8 @@ def test_workspace_process_basic(rule_runner) -> None: output_files=["capture-this-file", "will-not-capture-this-file"], cache_scope=ProcessCacheScope.PER_SESSION, # necessary to ensure result not cached from prior test runs ) - result = rule_runner.request(ProcessResult, [process]) - assert result.stdout.decode() == "this-goes-to-stdout\n" - assert result.stderr.decode() == "this-goes-to-stderr\n" - snapshot = rule_runner.request(Snapshot, [result.output_digest]) + result3 = rule_runner.request(ProcessResult, [process]) + assert result3.stdout.decode() == "this-goes-to-stdout\n" + assert result3.stderr.decode() == "this-goes-to-stderr\n" + snapshot = rule_runner.request(Snapshot, [result3.output_digest]) assert snapshot.files == ("capture-this-file",) diff --git a/src/rust/engine/Cargo.lock b/src/rust/engine/Cargo.lock index 2300dd746b2..867d1828563 100644 --- a/src/rust/engine/Cargo.lock +++ b/src/rust/engine/Cargo.lock @@ -1599,9 +1599,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "0.2.12" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -3434,9 +3434,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu",