Repository: conda/conda

Issue:
conda install --download-only feature
Right now we have `--dry-run`, but is there a way to actually download the `.tar.bz2` files into `pkgs` without installing them? If not, can there be? 

`--download-only` is one design with precedent, but also maybe as a separate subcommand, like `pip download`. The overarching goal is to get `pkgs` populated with all the dependencies necessary for an application and freezing it out into something akin to [a wheelhouse](https://pip.pypa.io/en/stable/reference/pip_wheel/#examples) for reproducible installs ([a la](https://www.paypal-engineering.com/2016/09/22/python-by-the-c-side/)). 

Thanks in advance!


Discussion:
Yeah we need this. It's been on the roadmap for the API for a while. Actually is trivial to implement at this point. I'll make sure it gets into 4.4. 

Sent from my iPhone

> On Sep 18, 2017, at 7:34 PM, Mahmoud Hashemi <notifications@github.com> wrote:
> 
> Right now we have --dry-run, but is there a way to actually download the .tar.bz2 files into pkgs without installing them? If not, can there be?
> 
> --download-only is one design with precedent, but also maybe as a separate subcommand, like pip download. The overarching goal is to get pkgs populated with all the dependencies necessary for an application and freezing it out into something akin to a wheelhouse for reproducible installs (a la).
> 
> Thanks in advance!
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub, or mute the thread.
> 


Resolving patch:
diff --git a/conda/base/context.py b/conda/base/context.py
--- a/conda/base/context.py
+++ b/conda/base/context.py
@@ -97,6 +97,7 @@ class Context(Configuration):
                                         element_type=string_types + (NoneType,),
                                         validation=default_python_validation)
     disallow = SequenceParameter(string_types)
+    download_only = PrimitiveParameter(False)
     enable_private_envs = PrimitiveParameter(False)
     force_32bit = PrimitiveParameter(False)
     max_shlvl = PrimitiveParameter(2)
@@ -682,6 +683,10 @@ def get_help_dict():
             Package specifications to disallow installing. The default is to allow
             all packages.
             """),
+        'download_only': dals("""
+            Solve an environment and ensure package caches are populated, but exit
+            prior to unlinking and linking packages into the prefix
+            """),
         'envs_dirs': dals("""
             The list of directories to search for named environments. When creating a new
             named environment, the environment will be placed in the first writable
diff --git a/conda/cli/conda_argparse.py b/conda/cli/conda_argparse.py
--- a/conda/cli/conda_argparse.py
+++ b/conda/cli/conda_argparse.py
@@ -1079,6 +1079,13 @@ def add_parser_create_install_update(p):
              "Can be used multiple times. "
              "Equivalent to a MatchSpec specifying a single 'provides_features'.",
     )
+    p.add_argument(
+        "--download-only",
+        action="store_true",
+        default=NULL,
+        help="Solve an environment and ensure package caches are populated, but exit "
+             "prior to unlinking and linking packages into the prefix.",
+    )
 
 
 def add_parser_pscheck(p):
diff --git a/conda/cli/install.py b/conda/cli/install.py
--- a/conda/cli/install.py
+++ b/conda/cli/install.py
@@ -10,21 +10,21 @@
 import os
 from os.path import abspath, basename, exists, isdir
 
-from conda.models.match_spec import MatchSpec
 from . import common
 from .common import check_non_admin
 from .._vendor.auxlib.ish import dals
 from ..base.constants import ROOT_ENV_NAME
 from ..base.context import context
-from ..common.compat import text_type, on_win
+from ..common.compat import on_win, text_type
 from ..core.envs_manager import EnvsDirectory
 from ..core.index import calculate_channel_urls, get_index
 from ..core.solve import Solver
-from ..exceptions import (CondaImportError, CondaOSError, CondaSystemExit, CondaValueError,
-                          DirectoryNotFoundError, DryRunExit, EnvironmentLocationNotFound,
-                          PackagesNotFoundError, TooManyArgumentsError,
-                          UnsatisfiableError)
+from ..exceptions import (CondaExitZero, CondaImportError, CondaOSError, CondaSystemExit,
+                          CondaValueError, DirectoryNotFoundError, DryRunExit,
+                          EnvironmentLocationNotFound, PackagesNotFoundError,
+                          TooManyArgumentsError, UnsatisfiableError)
 from ..misc import append_env, clone_env, explicit, touch_nonadmin
+from ..models.match_spec import MatchSpec
 from ..plan import (revert_actions)
 from ..resolve import ResolvePackageNotFound
 
@@ -267,6 +267,9 @@ def handle_txn(progressive_fetch_extract, unlink_link_transaction, prefix, args,
 
     try:
         progressive_fetch_extract.execute()
+        if context.download_only:
+            raise CondaExitZero('Package caches prepared. UnlinkLinkTransaction cancelled with '
+                                '--download-only option.')
         unlink_link_transaction.execute()
 
     except SystemExit as e:


Test patch:
diff --git a/tests/test_create.py b/tests/test_create.py
--- a/tests/test_create.py
+++ b/tests/test_create.py
@@ -141,7 +141,7 @@ def make_temp_env(*packages, **kwargs):
         try:
             # try to clear any config that's been set by other tests
             reset_context([os.path.join(prefix+os.sep, 'condarc')])
-            run_command(Commands.CREATE, prefix, *packages)
+            run_command(Commands.CREATE, prefix, *packages, **kwargs)
             yield prefix
         finally:
             rmtree(prefix, ignore_errors=True)
@@ -1455,6 +1455,14 @@ def test_force_remove(self):
         rm_rf(prefix)
         run_command(Commands.REMOVE, prefix, "--all")
 
+    def test_download_only_flag(self):
+        from conda.core.link import UnlinkLinkTransaction
+        with patch.object(UnlinkLinkTransaction, 'execute') as mock_method:
+            with make_temp_env('openssl --download-only', use_exception_handler=True) as prefix:
+                assert mock_method.call_count == 0
+            with make_temp_env('openssl', use_exception_handler=True) as prefix:
+                assert mock_method.call_count == 1
+
     def test_transactional_rollback_simple(self):
         from conda.core.path_actions import CreatePrefixRecordAction
         with patch.object(CreatePrefixRecordAction, 'execute') as mock_method: