Repository: NVIDIA/NVFlare

Issue:
Remove external 'zip' dependency from DistributionBuilder (zip_password=True)
Hi there, first off I want to acknowledge your efforts towards making an incredible tool, thank you!

The issue: I faced the exception *FileNotFoundError: [Errno 2] No such file or directory: 'zip'* when trying to specify `zip_password: true` for the DistributionBuilder args while running `provision -p project.yml`, and tracked the cause to `zip` not being installed on the local machine, which was a problem since installing additional software is not allowed on that machine.

## Replicate
```bash
# Launch a docker container with image python:3.8 running interactive bash
docker run -it --rm python:3.8 bash

# === Within docker container ===
pip install nvflare
provision # create example project.yml

# Set zip_password to true
sed -i 's/zip_password: false/zip_password: true/' project.yml

provision -p project.yml
```
We will find the following output:
```
Project yaml file: /project.yml.
Exception raised during provision.  Incomplete prod_n folder removed.
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/nvflare/lighter/spec.py", line 166, in provision
    b.build(study, ctx)
  File "/usr/local/lib/python3.8/site-packages/nvflare/lighter/impl/workspace.py", line 109, in build
    subprocess.run(run_args)
  File "/usr/local/lib/python3.8/subprocess.py", line 493, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/lib/python3.8/subprocess.py", line 858, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/local/lib/python3.8/subprocess.py", line 1704, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'zip'
```
Whereas it'll work if we install zip
```bash
# === Within docker container ===
apt-get update && apt-get install -y zip
provision -p project.yml
```

## Fix
The build function of the DistributionBuilder uses `shutil` to make the non-password protected zip but relies on `subprocess.run(['zip', ...])` in order to create password-protected files, since shutil doesn't support passwords.

I'd propose adding a package requirement of something like `pyminizip` or `pyzipper` to handle password protection.

https://github.com/NVIDIA/NVFlare/blob/c8d51bfb534c02eff874a760c47086c87cbff59f/nvflare/lighter/impl/workspace.py#L100-L113


Discussion:
I think another option is to either remove the password option or remove the DistributionBuilder completely.  That builder does nothing but zip the folder one by one.  Most likely, users can do that with their familiar tools.

Resolving patch:
diff --git a/nvflare/lighter/impl/workspace.py b/nvflare/lighter/impl/workspace.py
--- a/nvflare/lighter/impl/workspace.py
+++ b/nvflare/lighter/impl/workspace.py
@@ -98,6 +98,15 @@ def __init__(self, zip_password=False):
         self.zip_password = zip_password
 
     def build(self, project: Project, ctx: dict):
+        """Create a zip for each individual folder.
+        Note that if zip_password is True, the zip command will be used to encrypt zip files.  Users have to to
+        install this zip utility before provisioning.  In Ubuntu system, use this command to install zip utility:
+        sudo apt-get install zip
+
+        Args:
+            project (Project): project instance
+            ctx (dict): the provision context
+        """
         wip_dir = self.get_wip_dir(ctx)
         dirs = [name for name in os.listdir(wip_dir) if os.path.isdir(os.path.join(wip_dir, name))]
         for dir in dirs:
@@ -106,8 +115,12 @@ def build(self, project: Project, ctx: dict):
                 pw = generate_password()
                 run_args = ["zip", "-rq", "-P", pw, dest_zip_file + ".zip", ".", "-i", "startup/*"]
                 os.chdir(dest_zip_file)
-                subprocess.run(run_args)
-                os.chdir(os.path.join(dest_zip_file, ".."))
-                print(f"Password {pw} on {dir}.zip")
+                try:
+                    subprocess.run(run_args)
+                    print(f"Password {pw} on {dir}.zip")
+                except FileNotFoundError as e:
+                    raise RuntimeError("Unable to zip folders with password.  Maybe the zip utility is not installed.")
+                finally:
+                    os.chdir(os.path.join(dest_zip_file, ".."))
             else:
                 shutil.make_archive(dest_zip_file, "zip", root_dir=os.path.join(wip_dir, dir), base_dir="startup")


Test patch: