Dynamic Dependency Management for Modular Architecture
In modern software development, managing dependencies and configuring environments can often be a tedious and error-prone task. To simplify this process, we’ve revamped our install.sh script, which automates the installation of Python and Unix dependencies for our modular architecture. This enhanced script not only sets up a Python virtual environment but also intelligently reads configuration files to install dependencies only for active modules, streamlining the setup process.
Advantages of Dynamic Dependency Management
- Modular Architecture: Our system is designed to be modular, with each component encapsulated in a separate module. This approach allows for easier maintenance, testing, and scalability.
- Dependency Isolation: By specifying dependencies at the module level, we can ensure that each module has the necessary dependencies without affecting other modules.
- Automated Installation: The enhanced
install.sh
script automates the installation of dependencies based on the configuration files, reducing manual intervention and potential errors. Because only dependencies from active modules are installed, the setup process is optimized and efficient.
Background
To handle both Python and Unix (system) dependencies, we’ll make a few updates:
- Add a
dependencies
section to each module’s YAML configuration for listing Unix dependencies. - Use Python (specifically the
yaml
library) in theinstall.sh
script to read dependencies from each config file. - Ensure
apt-get install
is used for Unix dependencies, andpip install
for Python dependencies.
Here’s what this would look like:
Updated YAML Config Files
Each module config can now specify both Python and Unix dependencies.
Example servos.yml
:
1
2
3
4
5
6
7
8
9
10
11
servos:
port: /dev/ttyAMA0
conf:
leg_l_hip: { id: 0, pin: 9, range: [0, 180], start: 40 }
# other servo configurations...
dependencies:
python:
- pyserial
- pigpio
unix:
- libpigpio-dev
Example motion.yml
:
1
2
3
4
5
6
7
8
9
motion:
pin: 26
dependencies:
python:
- RPi.GPIO
unix:
- wiringpi
additional:
- "https://www.example.com/motion_install.md"
Modified install.sh
Script
The install script below:
- Parses
python
andunix
dependencies from each module’s YAML file. - Installs Unix dependencies with
apt-get install
and Python dependencies withpip install
. - Uses a Python helper embedded within the script to read YAML files (using
pyyaml
). - Outputs a summary of installed modules and dependencies. This includes any additional URLs for manual configuration.
Here’s the modified install.sh
script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/bin/bash
# Set up Python virtual environment
python3 -m venv --system-site-packages myenv
source myenv/bin/activate
# Initialize arrays for dependencies and additional setup URLs
PYTHON_DEPENDENCIES=()
UNIX_DEPENDENCIES=()
ADDITIONAL_URLS=()
ACTIVE_MODULES=()
# Helper function to parse dependencies from YAML files using Python
parse_dependencies() {
myenv/bin/python3 - <<EOF
import yaml, sys, os
config_file = "$1"
module_name = os.path.basename(config_file).replace('.yml', '') # Get the module name from the filename
try:
with open(config_file) as f:
config = yaml.safe_load(f)
if isinstance(config, dict):
for section in config.values():
# Ensure each section has 'enabled' set to true and 'dependencies' exists
if isinstance(section, dict) and section.get('enabled', False) and 'dependencies' in section:
print(f"MODULE:{module_name}")
for dep_type, deps in section['dependencies'].items():
if dep_type == 'python':
for dep in deps:
print(f"PYTHON:{dep}")
elif dep_type == 'unix':
for dep in deps:
print(f"UNIX:{dep}")
elif dep_type == 'additional':
for url in deps:
print(f"ADDITIONAL:{module_name}:{url}")
except yaml.YAMLError as e:
print(f"Error reading {config_file}: {e}", file=sys.stderr)
EOF
}
# Iterate over each YAML config file in the config directory
for config_file in config/*.yml; do
while IFS= read -r dependency; do
# Separate Python and Unix dependencies and capture active module names
if [[ $dependency == MODULE:* ]]; then
ACTIVE_MODULES+=("${dependency#MODULE:}")
elif [[ $dependency == PYTHON:* ]]; then
PYTHON_DEPENDENCIES+=("${dependency#PYTHON:}")
elif [[ $dependency == UNIX:* ]]; then
UNIX_DEPENDENCIES+=("${dependency#UNIX:}")
elif [[ $dependency == ADDITIONAL:* ]]; then
ADDITIONAL_URLS+=("${dependency#ADDITIONAL:}")
fi
done < <(parse_dependencies "$config_file")
done
# Remove duplicate dependencies
UNIQUE_PYTHON_DEPENDENCIES=($(echo "${PYTHON_DEPENDENCIES[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
UNIQUE_UNIX_DEPENDENCIES=($(echo "${UNIX_DEPENDENCIES[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
UNIQUE_ACTIVE_MODULES=($(echo "${ACTIVE_MODULES[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
UNIQUE_ADDITIONAL_URLS=($(echo "${ADDITIONAL_URLS[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
# Update apt-get and install Unix dependencies
if [ ${#UNIQUE_UNIX_DEPENDENCIES[@]} -ne 0 ]; then
sudo apt-get update
for dep in "${UNIQUE_UNIX_DEPENDENCIES[@]}"; do
sudo apt-get install -y "$dep"
done
fi
# Install Python dependencies explicitly using the virtual environment's pip
for dep in "${UNIQUE_PYTHON_DEPENDENCIES[@]}"; do
myenv/bin/python3 -m pip install "$dep"
done
# Set execute permissions for additional scripts
chmod 777 startup.sh stop.sh
# Summary of modules and dependencies installed
echo -e "\n==== Installation Summary ===="
echo "Active modules installed: ${#UNIQUE_ACTIVE_MODULES[@]}"
for module in "${UNIQUE_ACTIVE_MODULES[@]}"; do
echo " - $module"
done
echo -e "\nPython dependencies installed:"
for dep in "${UNIQUE_PYTHON_DEPENDENCIES[@]}"; do
echo " - $dep"
done
echo -e "\nUnix dependencies installed:"
for dep in "${UNIQUE_UNIX_DEPENDENCIES[@]}"; do
echo " - $dep"
done
if [ ${#UNIQUE_ADDITIONAL_URLS[@]} -ne 0 ]; then
echo -e "\nACTION REQUIRED: Additional manual configuration required for the following modules:"
for dep in "${UNIQUE_ADDITIONAL_URLS[@]}"; do
echo " - $dep"
done
fi
echo "============================="
Explanation of Changes
parse_dependencies
Function: A Python function is embedded in theinstall.sh
script to parse YAML files and print dependencies in a formatted way for easy processing.- Dependency Arrays:
PYTHON_DEPENDENCIES
andUNIX_DEPENDENCIES
store dependencies, allowing us to separate outapt-get
andpip
installations.
- Removing Duplicates: Using
sort -u
to ensure dependencies aren’t installed more than once. - Installing Dependencies:
- Unix dependencies are updated and installed using
apt-get
. - Python dependencies are installed with
pip
.
- Unix dependencies are updated and installed using
Folder Structure Example
Here’s the directory structure for this configuration:
1
2
3
4
5
6
project_root/
├── config/
│ ├── servos.yml
│ ├── motion.yml
├── install.sh
└── main.py
This approach ensures that dependencies are handled based on each module’s needs, making it easier to maintain and update dependencies dynamically.
Example Output
After running the install.sh
script, you should see a summary of the installed modules and dependencies.
Remember, only active modules will have their dependencies installed.
```plaintext ==== Installation Summary ==== Active modules installed: 13
- animate
- braillespeak
- buzzer
- motion
- neopixel
- piservo
- pitemperature
- serial
- servos
- tracking
- translator
- universalremote
- vision
Python dependencies installed:
- adafruit-circuitpython-seesaw
- googletrans==3.1.0a0
- gpiozero
- lirc
- pigpio
- pubsub
- pypubsub
- python3-munkres
- python3-opencv
Unix dependencies installed:
- imx500-all
- lirc
ACTION REQUIRED: Additional manual configuration required for the following modules:
- motion:https://www.example.com/motion_install.md
Read More
The initial modular architecture for this approach was detailed in an earlier blog post. You can read more about it here: Dynamic Module Loading in Python.