From b262b2e0dcd91ec3684a5aa3a45bd267f1f2bc0a Mon Sep 17 00:00:00 2001 From: thecookingsenpai Date: Mon, 25 Dec 2023 13:28:06 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 + README.md | 53 ++++++ config.json | 20 +++ nvenv.py | 159 ++++++++++++++++++ pip_package/LICENSE | 0 pip_package/MANIFEST.in | 0 pip_package/README.md | 47 ++++++ pip_package/pyproject.toml | 0 pip_package/src/nixvenv.egg-info/PKG-INFO | 4 + pip_package/src/nixvenv.egg-info/SOURCES.txt | 10 ++ .../src/nixvenv.egg-info/dependency_links.txt | 1 + .../src/nixvenv.egg-info/top_level.txt | 1 + pip_package/src/nixvenv/__init__.py | 1 + pip_package/src/nixvenv/__main__.py | 159 ++++++++++++++++++ .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 197 bytes .../__pycache__/__main__.cpython-39.pyc | Bin 0 -> 4412 bytes test | 0 17 files changed, 459 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.json create mode 100644 nvenv.py create mode 100644 pip_package/LICENSE create mode 100644 pip_package/MANIFEST.in create mode 100644 pip_package/README.md create mode 100644 pip_package/pyproject.toml create mode 100644 pip_package/src/nixvenv.egg-info/PKG-INFO create mode 100644 pip_package/src/nixvenv.egg-info/SOURCES.txt create mode 100644 pip_package/src/nixvenv.egg-info/dependency_links.txt create mode 100644 pip_package/src/nixvenv.egg-info/top_level.txt create mode 100644 pip_package/src/nixvenv/__init__.py create mode 100644 pip_package/src/nixvenv/__main__.py create mode 100644 pip_package/src/nixvenv/__pycache__/__init__.cpython-39.pyc create mode 100644 pip_package/src/nixvenv/__pycache__/__main__.cpython-39.pyc create mode 100644 test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54b39c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +alternative.json +localenv +altenv +.cache \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..96f1a1b --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# NixVenv + +## A pythonesque venv like approach to system virtual environments + +### What is this? + +Just as Python's venv allows developers to specify virtual environments for their python projects, NixVenv goal is to provide developers with the possibility to create virtual environments for their whole system. + +Have you ever met a dependency that is required but would break your code elsewhere in your system? With NixVenv you can create a virtual environment for that specific application and run it with customized files, environment variables and configurations. Once you have finished, just deactivate it and you will be back in your usual shell environment. + +### Features + +• Customizable environment variables +• Easy to set up configuration +• Local files support +• Local binaries support and priority + +### Installation + +To install NixVenv, execute in a terminal: + + git clone https://github.com/thecookingsenpai/nixvenv + cd nixvenv + pip install -r requirements.txt + +Alternatively, you can install NixVenv using pip: + + pip install nixvenv + +Or by building the pip package yourself: + + git clone https://github.com/thecookingsenpai/nixvenv + cd nixvenv + python -m pip install -e pip_package + +In both these cases, you will have to provide (or copy from here) your own config.json file + +### Usage + + python nvenv.py operation= config_file= + +### Command Line Arguments + +#### operation= + +• run: activate the nvenv environment specified by the configuration +• activate: same as above +• new: create and activate the nvenv environment specified by the configuration if not already present +• remove: remove the nvenv environment specified by the configuration + +#### config_file= + +Any valid json file containing a nvenv configuration (relative path). diff --git a/config.json b/config.json new file mode 100644 index 0000000..2392fc7 --- /dev/null +++ b/config.json @@ -0,0 +1,20 @@ +{ + "nvenv_name": "localenv", + "working_dir": "./localenv", + "local_bin": "bin", + "load_file": "load.sh", + "init_cmds": { + "nvenv_folder": "mkdir test", + "local_bin_folder": "echo '#!/bin/bash \necho It is working!' > nvenv_test && chmod +x nvenv_test", + "here": "mkdir .cache" + }, + "overwrites" : { + "IS_VENV": "true" + }, + "files": [ + ["./test", ".cache/test"] + ], + "additions" : { + "PATH": "/usr/local/sbin:/home/tcsenpai/bin" + } +} \ No newline at end of file diff --git a/nvenv.py b/nvenv.py new file mode 100644 index 0000000..4492148 --- /dev/null +++ b/nvenv.py @@ -0,0 +1,159 @@ +import os +import subprocess +import json +from pprint import pprint +import sys +import shutil + +environment = os.environ.copy() +config = {} +nvenv_name = "" +local_bin_folder = "" +load_file = "" +hostname = "" +username = "" + +# INFO Entry point +def main(config_file="./config.json", new=False, delete=False): + print("[ Welcome to NixVenv, the *nix compatible virtual shell environment manager ]") + global environment, config, cmds, local_bin_folder, load_file, nvenv_name, shell, hostname, username + p = subprocess.run("whoami", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + username = p.stdout.decode('utf-8').strip() + p = subprocess.run("hostname", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + hostname = p.stdout.decode('utf-8').strip() + p = subprocess.run("echo $SHELL", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + shell = p.stdout.decode('utf-8').strip() + print(shell) + cmds = 'echo "[ Setting environment variables ]"\n' + config = open(config_file, "r") + config = json.loads(config.read()) + print("[*] Config file loaded: " + config_file) + nvenv_name = config["nvenv_name"] + # Halt if we have to delete the environment + if delete: + if not os.path.exists(nvenv_name): + print("[*] Environment does not exist: " + nvenv_name) + else: + shutil.rmtree(nvenv_name, ignore_errors=True) + print("[*] Deleted environment: " + nvenv_name) + os._exit(0) + # Else we use the environment + print("[*] Reading from environment: " + nvenv_name) + local_bin_folder = os.getcwd() + "/" + nvenv_name + "/" + config["local_bin"] + print("[*] Local bin folder: " + local_bin_folder) + load_file = os.getcwd() + "/" + nvenv_name + "/" + config["load_file"] + new_environment(new) + load_config() + load_environment() + +# INFO Load a configuration file +def load_config(): + global cmds, local_bin_folder, load_file, hostname + # Overwrites replace environment variables fully or create a new one + overwrites = config['overwrites'] + for key in overwrites: + cmds += "export " + key + "=" + overwrites[key] + "\n" + print("[*] Overwriting enviromental variable: " + key) + # Additions add the value to the environment variable preserving the original value + additions = config["additions"] + for key in additions: + cmds += "export " + key + "=${" + key + "}:" + additions[key] + "\n" + print("[*] Adding to enviromental variable: " + key) + # Setting our environment variables + print("[*] Setting our environment variables") + cmds += "export PATH=" + local_bin_folder + ":" + os.environ["PATH"] + # Loading the static environment variables defined in the config file + print("[*] Setting the environment name as " + config["nvenv_name"]) + cmds += "export NVENV_NAME=" + config["nvenv_name"] + "\n" + print("[*] Changing to: " + config["working_dir"]) + cmds += "cd " + config["working_dir"] + "\n" + cmds += 'echo "nvenv environment loaded: $NVENV_NAME"\n' + # Setting up customizations + cmds += 'export PS1="' + username + ' [' + config["nvenv_name"] + '@' + hostname+ '] :> "\n' + # Finally, writing to the load file + print("[*] Writing to the load file: " + load_file) + with open(load_file, "w") as f: + f.write(cmds) + +# INFO Loading the local environment fully +def load_environment(): + print("[*] Loading the environment") + global environment, load_file, shell + # Multiple shells are supported + if "zsh" in shell: + cmd = "source " + load_file + " && " + shell + " -f -d" + else: + cmd = shell + " --rcfile " + load_file + # Finally, loading the environment by sourcing the load file + print("[+] Executing: " + cmd) + print("\nNOTE: Digit 'exit' and press ENTER to return to the previous environment\n") + os.system(cmd) + +# INFO Environemnt creation +def new_environment(new_flag=False): + global cmds, local_bin_folder + if os.path.exists(os.getcwd() + "/" + nvenv_name): + if not new_flag: + raise Exception("[x] Environment already exists: " + nvenv_name) + print("[+] Environment already exists: " + nvenv_name) + return + # Creating the local_bin_folder if it doesn't exist + if not os.path.exists(local_bin_folder): + os.makedirs(local_bin_folder) + # Reading the init commands + init_cmds_list = config["init_cmds"] + for cmd in init_cmds_list: + # Replacing folders if needed + if (cmd == "here"): + running_dir = os.getcwd() + elif (cmd == "nvenv_folder"): + running_dir = os.getcwd() + "/" + nvenv_name + elif (cmd == "local_bin_folder"): + running_dir = local_bin_folder + else: + # Support for custom directories + # REVIEW Security note: limit it to the current working directory + if not os.path.exists(running_dir): + raise Exception("Running directory does not exist: " + running_dir) + # Compiling the command to be executed before the environment is activated + single_cmd = init_cmds_list[cmd] + actual_cmd = "cd '" + running_dir + "' && " + single_cmd + print("[*] Executing: " + actual_cmd) + os.system(actual_cmd) + # Support files + files = config["files"] + for file in files: + origin = file[0] + destination = file[1] + if not os.path.exists(origin): + raise Exception("[x] Origin file does not exist: " + origin) + if os.path.exists(destination): + raise Exception("[x] Destination file already exists: " + destination) + print("[*] Copying file: " + origin + " to: " + destination) + shutil.copy(origin, destination) + +if __name__ == '__main__': + # Argument parsing + args_len = len(sys.argv) + print(sys.argv) + filename = sys.argv[0] + params = { + "operation": "run", + "config_file": "config.json", + } + # Getting parameters + for param in sys.argv[1:]: + if "operation=" in param: + params["operation"] = param.split("=")[1] + if "config_file=" in param: + params["config_file"] = param.split("=")[1] + # Sanity checks + if not os.path.exists(params["config_file"]): + raise Exception("[x] Config file does not exist: " + params["config_file"]) + # Runs based on operation + if params["operation"] == "activate" or params["operation"] == "run": + main(config_file=params["config_file"]) + elif params["operation"] == "new": + main(config_file=params["config_file"], new=True) + elif params["operation"] == "remove": + main(config_file=params["config_file"], delete=True) \ No newline at end of file diff --git a/pip_package/LICENSE b/pip_package/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/pip_package/MANIFEST.in b/pip_package/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/pip_package/README.md b/pip_package/README.md new file mode 100644 index 0000000..6f4df46 --- /dev/null +++ b/pip_package/README.md @@ -0,0 +1,47 @@ +# NixVenv + +## A pythonesque venv like approach to system virtual environments + +### What is this? + +Just as Python's venv allows developers to specify virtual environments for their python projects, NixVenv goal is to provide developers with the possibility to create virtual environments for their whole system. + +Have you ever met a dependency that is required but would break your code elsewhere in your system? With NixVenv you can create a virtual environment for that specific application and run it with customized files, environment variables and configurations. Once you have finished, just deactivate it and you will be back in your usual shell environment. + +### Features + +• Customizable environment variables +• Easy to set up configuration +• Local files support +• Local binaries support and priority + +### Installation + +To install NixVenv, execute in a terminal: + + git clone https://github.com/thecookingsenpai/nixvenv + cd nixvenv + pip install -r requirements.txt + +Alternatively, you can install NixVenv using pip: + + pip install nixvenv + +In this case, you will have to provide (or copy from here) your own config.json file + +### Usage + + python nvenv.py operation= config_file= + +### Command Line Arguments + +#### operation= + +• run: activate the nvenv environment specified by the configuration +• activate: same as above +• new: create and activate the nvenv environment specified by the configuration if not already present +• remove: remove the nvenv environment specified by the configuration + +#### config_file= + +Any valid json file containing a nvenv configuration (relative path). diff --git a/pip_package/pyproject.toml b/pip_package/pyproject.toml new file mode 100644 index 0000000..e69de29 diff --git a/pip_package/src/nixvenv.egg-info/PKG-INFO b/pip_package/src/nixvenv.egg-info/PKG-INFO new file mode 100644 index 0000000..35c7116 --- /dev/null +++ b/pip_package/src/nixvenv.egg-info/PKG-INFO @@ -0,0 +1,4 @@ +Metadata-Version: 2.1 +Name: nixvenv +Version: 0.0.0 +License-File: LICENSE diff --git a/pip_package/src/nixvenv.egg-info/SOURCES.txt b/pip_package/src/nixvenv.egg-info/SOURCES.txt new file mode 100644 index 0000000..c4c123f --- /dev/null +++ b/pip_package/src/nixvenv.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +LICENSE +MANIFEST.in +README.md +pyproject.toml +src/nixvenv/__init__.py +src/nixvenv/__main__.py +src/nixvenv.egg-info/PKG-INFO +src/nixvenv.egg-info/SOURCES.txt +src/nixvenv.egg-info/dependency_links.txt +src/nixvenv.egg-info/top_level.txt \ No newline at end of file diff --git a/pip_package/src/nixvenv.egg-info/dependency_links.txt b/pip_package/src/nixvenv.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pip_package/src/nixvenv.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pip_package/src/nixvenv.egg-info/top_level.txt b/pip_package/src/nixvenv.egg-info/top_level.txt new file mode 100644 index 0000000..2d6f58e --- /dev/null +++ b/pip_package/src/nixvenv.egg-info/top_level.txt @@ -0,0 +1 @@ +nixvenv diff --git a/pip_package/src/nixvenv/__init__.py b/pip_package/src/nixvenv/__init__.py new file mode 100644 index 0000000..b3c06d4 --- /dev/null +++ b/pip_package/src/nixvenv/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" \ No newline at end of file diff --git a/pip_package/src/nixvenv/__main__.py b/pip_package/src/nixvenv/__main__.py new file mode 100644 index 0000000..4492148 --- /dev/null +++ b/pip_package/src/nixvenv/__main__.py @@ -0,0 +1,159 @@ +import os +import subprocess +import json +from pprint import pprint +import sys +import shutil + +environment = os.environ.copy() +config = {} +nvenv_name = "" +local_bin_folder = "" +load_file = "" +hostname = "" +username = "" + +# INFO Entry point +def main(config_file="./config.json", new=False, delete=False): + print("[ Welcome to NixVenv, the *nix compatible virtual shell environment manager ]") + global environment, config, cmds, local_bin_folder, load_file, nvenv_name, shell, hostname, username + p = subprocess.run("whoami", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + username = p.stdout.decode('utf-8').strip() + p = subprocess.run("hostname", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + hostname = p.stdout.decode('utf-8').strip() + p = subprocess.run("echo $SHELL", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + shell = p.stdout.decode('utf-8').strip() + print(shell) + cmds = 'echo "[ Setting environment variables ]"\n' + config = open(config_file, "r") + config = json.loads(config.read()) + print("[*] Config file loaded: " + config_file) + nvenv_name = config["nvenv_name"] + # Halt if we have to delete the environment + if delete: + if not os.path.exists(nvenv_name): + print("[*] Environment does not exist: " + nvenv_name) + else: + shutil.rmtree(nvenv_name, ignore_errors=True) + print("[*] Deleted environment: " + nvenv_name) + os._exit(0) + # Else we use the environment + print("[*] Reading from environment: " + nvenv_name) + local_bin_folder = os.getcwd() + "/" + nvenv_name + "/" + config["local_bin"] + print("[*] Local bin folder: " + local_bin_folder) + load_file = os.getcwd() + "/" + nvenv_name + "/" + config["load_file"] + new_environment(new) + load_config() + load_environment() + +# INFO Load a configuration file +def load_config(): + global cmds, local_bin_folder, load_file, hostname + # Overwrites replace environment variables fully or create a new one + overwrites = config['overwrites'] + for key in overwrites: + cmds += "export " + key + "=" + overwrites[key] + "\n" + print("[*] Overwriting enviromental variable: " + key) + # Additions add the value to the environment variable preserving the original value + additions = config["additions"] + for key in additions: + cmds += "export " + key + "=${" + key + "}:" + additions[key] + "\n" + print("[*] Adding to enviromental variable: " + key) + # Setting our environment variables + print("[*] Setting our environment variables") + cmds += "export PATH=" + local_bin_folder + ":" + os.environ["PATH"] + # Loading the static environment variables defined in the config file + print("[*] Setting the environment name as " + config["nvenv_name"]) + cmds += "export NVENV_NAME=" + config["nvenv_name"] + "\n" + print("[*] Changing to: " + config["working_dir"]) + cmds += "cd " + config["working_dir"] + "\n" + cmds += 'echo "nvenv environment loaded: $NVENV_NAME"\n' + # Setting up customizations + cmds += 'export PS1="' + username + ' [' + config["nvenv_name"] + '@' + hostname+ '] :> "\n' + # Finally, writing to the load file + print("[*] Writing to the load file: " + load_file) + with open(load_file, "w") as f: + f.write(cmds) + +# INFO Loading the local environment fully +def load_environment(): + print("[*] Loading the environment") + global environment, load_file, shell + # Multiple shells are supported + if "zsh" in shell: + cmd = "source " + load_file + " && " + shell + " -f -d" + else: + cmd = shell + " --rcfile " + load_file + # Finally, loading the environment by sourcing the load file + print("[+] Executing: " + cmd) + print("\nNOTE: Digit 'exit' and press ENTER to return to the previous environment\n") + os.system(cmd) + +# INFO Environemnt creation +def new_environment(new_flag=False): + global cmds, local_bin_folder + if os.path.exists(os.getcwd() + "/" + nvenv_name): + if not new_flag: + raise Exception("[x] Environment already exists: " + nvenv_name) + print("[+] Environment already exists: " + nvenv_name) + return + # Creating the local_bin_folder if it doesn't exist + if not os.path.exists(local_bin_folder): + os.makedirs(local_bin_folder) + # Reading the init commands + init_cmds_list = config["init_cmds"] + for cmd in init_cmds_list: + # Replacing folders if needed + if (cmd == "here"): + running_dir = os.getcwd() + elif (cmd == "nvenv_folder"): + running_dir = os.getcwd() + "/" + nvenv_name + elif (cmd == "local_bin_folder"): + running_dir = local_bin_folder + else: + # Support for custom directories + # REVIEW Security note: limit it to the current working directory + if not os.path.exists(running_dir): + raise Exception("Running directory does not exist: " + running_dir) + # Compiling the command to be executed before the environment is activated + single_cmd = init_cmds_list[cmd] + actual_cmd = "cd '" + running_dir + "' && " + single_cmd + print("[*] Executing: " + actual_cmd) + os.system(actual_cmd) + # Support files + files = config["files"] + for file in files: + origin = file[0] + destination = file[1] + if not os.path.exists(origin): + raise Exception("[x] Origin file does not exist: " + origin) + if os.path.exists(destination): + raise Exception("[x] Destination file already exists: " + destination) + print("[*] Copying file: " + origin + " to: " + destination) + shutil.copy(origin, destination) + +if __name__ == '__main__': + # Argument parsing + args_len = len(sys.argv) + print(sys.argv) + filename = sys.argv[0] + params = { + "operation": "run", + "config_file": "config.json", + } + # Getting parameters + for param in sys.argv[1:]: + if "operation=" in param: + params["operation"] = param.split("=")[1] + if "config_file=" in param: + params["config_file"] = param.split("=")[1] + # Sanity checks + if not os.path.exists(params["config_file"]): + raise Exception("[x] Config file does not exist: " + params["config_file"]) + # Runs based on operation + if params["operation"] == "activate" or params["operation"] == "run": + main(config_file=params["config_file"]) + elif params["operation"] == "new": + main(config_file=params["config_file"], new=True) + elif params["operation"] == "remove": + main(config_file=params["config_file"], delete=True) \ No newline at end of file diff --git a/pip_package/src/nixvenv/__pycache__/__init__.cpython-39.pyc b/pip_package/src/nixvenv/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ecd35512c5f10c78894cafce6c08bc5d7450a06 GIT binary patch literal 197 zcmYe~<>g`k0z3YzDWX97F^Gc<7=auIATH(r5-AK(3@MDk44O<;tOj}pdWL?QjJLSs z?<+0|3EFHE;j` literal 0 HcmV?d00001 diff --git a/pip_package/src/nixvenv/__pycache__/__main__.cpython-39.pyc b/pip_package/src/nixvenv/__pycache__/__main__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54149c911b62219b604e523fb9a4f55d9566f8d8 GIT binary patch literal 4412 zcmai1&u<&Y72erhlFKDUQnD94WQMs1_L37NZ2kO{c&-p8QDNyXSrvN?nFJDSzCw!aefoRcqp&6pA08c{E4>XTFI62JVl3Hp^0{mSUBqd%P>-z@p9_zwN4yuhdXcYC2@4OV-d^fM;dHY~zxCCyd*5iUn_m41Y)tPEAAlerVPsxmNm z`PdAqYHCxD!Fy(*M^xn+yf@++wkpbK1DVHtv05q>ERr&pO7SjHzro+PYgaiZ_tV!s}u|KMz8?B3u<9>`t^;Z77w(T0Yx zOwuN8xg)wl2|lU9x+f8nMtX(lpNW8zirX^oon?B-*8Fle?gZWTlQ2r>i2EK*Jxsmr zxXXovl3)pVn-p(d&I);YxXJ#LzKlB}Nis|BN16TUuRq3LD(w12 z*;7*Bpf(6_-HuS5XFO}1Ua6hmK~9mJdL*8;C&D4C=zEP$$0l5Ig50vRN>0;P&n#@G ztC6uY*8iI!Tafzl^R>@$y=1K;;iE8!;%yluNf-$guHBJw|4Dqfb~`2qtRYUx+iU%> z-|h#Ur-;S1M0QTs+wEQuM(y_7{pXoYpVK%Xc@Lez9CMB}*tj{&W-hC-D%NfEHIu$w zv&xn*E-{A{nS2xbI<$HGBhVrjXwdr22(+5Ukue}Jf9r@T#?1jk`daf?8!$>sZqFM( zHv*Lj)CXt|Y4*dJ`o*~Z`$@5{-=KRqHzt0T=m{qW&O8!y>9(imUq3LP@uO&B|o7apau7bKp} z&U9uZVe5;{tuNYJAAYjAkxtY0+q*%ulWV!|`ITp}e2Q6{hcdM~+)Gz;2-N_1Mm{AX zf#b?6k<0EYO&`3ov7DOTqnGT1wDj0pzvY3df$mFKdt?^UndE4=g(cauRz=Pu_T@$D zYSg_>-6eEcX>>g~OG_H12r=Xw5ewRhEnB`tGwZ2%F4yRbZEFC?yX(aM6LjyQC0EfI zmV-20LTb{uqQ^zE!Df-hvq;+&Bx{xFJ!g`XaN9!DN%{~U2vz+ma#Jx>T31mIm0Uk#?>t6b?yLJEJ=DK$$+zFL; zjd1yz7ew6aOFR|c=GMc_&nRxCQ2R2XFLD)#2VuOQOvZuRD&&5m2SX!x?yO?+JW-;D z$I!wfa^43kV7`x*kc$DE@WPx~g{>VXiPjZmR3b`-u)!IZfTkYob}kbFU4$qP?S+0W%4 zSDkvpTzEvg6P(hLWfd%=0haS!VfJu4`*Z`IIkT?`jlP}_=`apvrZk$uf zM3L`-(ioIaZ81$;dVaV``!fI?v&k!zfhkyoA^lQux)pj2gZ6l-TL=XZuk^p$)(h9Tm&x-uZeewShy| z(!w(haJ%;v9Iteb9%v3fh#6Cy4@h|E9g*R zL4M2i9jbb^yTOiMJGEN7i&{m#4>rGo+KtRaxa_;ATXwpFEbF^LhpK4$mcP&fNztqr z>-+l^{_2+Jb8s2#kI|A_=nQiK#R_4ajVyEt4bfcW;|yBS1_kp5acHC@w4`E&+ANbK zD|5z`k!)a^j>N~JRcn~PjV)+3C?+;t%t4UUrqKEf>ymk!?ElX0pb@y(I-EF;=d$o=Qxj>zXF0+7S z+F6onJo!s>er4jN&0R{q&)+KjCh6Bnzf)A->Gwke+o`4Kd2Y8etBcaw4&=@OE+BW3 zHs(5RGKU;VT=-u%kU=l;3wlJo-tUHLBK9`u1gdf;I(q3c=VmYF`(1I1@*?>N9l6bc s%gDVh9De#|xvx8yotk^y#X9~>lbQc37RdS3X3ZJaFqaSjHOy=5|I%k!kN^Mx literal 0 HcmV?d00001 diff --git a/test b/test new file mode 100644 index 0000000..e69de29