Files
docker-php-fpm/bin/gen-php-modules.py
2022-12-08 12:41:44 +01:00

288 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generate Ansible group_vars from module definition."""
import os
import sys
from collections import OrderedDict
from typing import Dict, List, Any
import yaml
# --------------------------------------------------------------------------------------------------
# GLOBALS
# --------------------------------------------------------------------------------------------------
SCRIPT_PATH = str(os.path.dirname(os.path.realpath(__file__)))
REPOSITORY_PATH = str(os.path.dirname(SCRIPT_PATH))
PHP_MODULE_PATH = str(os.path.join(REPOSITORY_PATH, "php_modules"))
GROUP_VARS_PATH = str(os.path.join(REPOSITORY_PATH, ".ansible", "group_vars", "all"))
# --------------------------------------------------------------------------------------------------
# HELPER FUNCTIONS
# --------------------------------------------------------------------------------------------------
def get_el_by_name(items: List[Dict[str, Any]], name: str) -> Dict[str, Any]:
"""Returns an element from a dict list by its 'name' key with given value."""
for item in items:
if item["name"] == name:
return item
print("error, key name not found by value", name, "in list: ", items)
sys.exit(1)
def load_yaml(path: str) -> Dict[str, Any]:
"""Load yaml file and return its dict()."""
with open(path, "r", encoding="utf8") as fp:
data = yaml.safe_load(fp)
return data
def load_yaml_raw(path: str, indent: int = 0) -> str:
"""Load and returns yaml file as str."""
lines = []
with open(path, "r", encoding="utf8") as fp:
for line in fp:
# Remove: empty lines and ---
if line in ("---\n", "---\r\n", "\n", "\r\n"):
continue
# Remove: comments
if line.startswith("#"):
continue
lines.append(" " * indent + line)
return "".join(lines)
# --------------------------------------------------------------------------------------------------
# MODULE FUNCTIONS
# --------------------------------------------------------------------------------------------------
def get_module_options(module_dirname: str) -> Dict[str, Any]:
"""Returns yaml dict options of a PHP module given by its absolute file path."""
return load_yaml(os.path.join(PHP_MODULE_PATH, module_dirname, "options.yml"))
def get_module_build(module_dirname: str) -> Dict[str, Any]:
"""Returns yaml dict build configuration of a PHP module given by its absolute file path."""
return load_yaml(os.path.join(PHP_MODULE_PATH, module_dirname, "build.yml"))
def get_module_test(module_dirname: str) -> Dict[str, Any]:
"""Returns yaml dict test configuration of a PHP module given by its absolute file path."""
return load_yaml(os.path.join(PHP_MODULE_PATH, module_dirname, "test.yml"))
def get_modules(selected_modules: List[str], ignore_dependencies: bool) -> List[Dict[str, Any]]:
"""Returns a list of PHP module directory names.
Args:
selected_modules: If not empty, only gather specified modules (and its dependencies).
ignore_dependencies: If true, all dependent extensions will be ignored.
"""
modules = []
with os.scandir(PHP_MODULE_PATH) as it:
for item in it:
if not item.name.startswith(".") and item.is_dir():
data = get_module_options(item.name)
modules.append(
{
"dir": item.name,
"name": data["name"],
"deps": data["depends_build"],
"exclude": data["exclude"],
}
)
# Convert list of deps into dict(dir, name, deps)
items = []
for module in modules:
if module["deps"] and not ignore_dependencies:
deps = []
for dep in module["deps"]:
deps.append(get_el_by_name(modules, dep))
module["deps"] = deps
items.append(module)
else:
module["deps"] = []
items.append(module)
# Check if we only want to read a single module
if selected_modules:
return [get_el_by_name(items, mod_name) for mod_name in selected_modules]
return sorted(items, key=lambda item: item["dir"])
def get_module_dependency_tree(modules: List[Dict[str, Any]]) -> OrderedDict[str, Any]:
"""Returns dictionary of module dependency tree."""
module_tree = OrderedDict() # type: OrderedDict[str, Any]
for module in modules:
mod_name = module["name"]
mod_deps = module["deps"]
module_tree[mod_name] = {}
# Do we have module requirements?
if len(mod_deps) > 0:
module_tree[mod_name] = get_module_dependency_tree(mod_deps)
return module_tree
def resolve_module_dependency_tree(tree: OrderedDict[str, Any]) -> List[str]:
"""Returns sorted list of resolved dependencies."""
resolved = []
for key, _ in tree.items():
# Has dependenies
if tree[key]:
childs = resolve_module_dependency_tree(tree[key])
for child in childs:
if child not in resolved:
resolved.append(child)
# Add current node, if not already available
if key not in resolved:
resolved.append(key)
return resolved
# --------------------------------------------------------------------------------------------------
# PRINT FUNCTIONS
# --------------------------------------------------------------------------------------------------
def print_modules(modules: List[Dict[str, Any]]) -> None:
"""Print directory modules."""
for module in modules:
print(module["dir"] + "/")
print(" name:", module["name"])
print(" deps:", end=" ")
for dep in module["deps"]:
print(dep["name"], end=", ")
print()
print(" excl:", module["exclude"])
def print_dependency_tree(tree: Dict[str, Any], lvl: int = 0) -> None:
"""Print dependency tree of modules."""
for key, value in tree.items():
print(" " * lvl, "-", key)
if value:
print_dependency_tree(tree[key], lvl + 2)
# --------------------------------------------------------------------------------------------------
# WRITE ANSIBLE GROUP_VARS FUNCTIONS
# --------------------------------------------------------------------------------------------------
def write_group_vars(modules: List[str]) -> None:
"""Write mods.yml group_vars for ansible."""
group_vars = os.path.join(GROUP_VARS_PATH, "mods.yml")
with open(group_vars, "w", encoding="utf8") as fp:
fp.write("---\n\n")
fp.write("# DO NOT ALTER THIS FILE - IT IS AUTOGENERATED.\n\n")
# Enabled modules
fp.write("# The following specifies the order in which modules are being built.\n")
fp.write("extensions_enabled:\n")
for module in modules:
fp.write(" - " + module + "\n")
fp.write("\n\n")
# Build defines modules
fp.write("# The following specifies how modules are being built.\n")
fp.write("extensions_available:\n")
for module in modules:
opts = get_module_options(module)
fp.write(" " + module + ":\n")
fp.write(" disabled: [" + ", ".join(str(x) for x in opts["exclude"]) + "]\n")
fp.write(load_yaml_raw(os.path.join(PHP_MODULE_PATH, module, "build.yml"), 4))
# --------------------------------------------------------------------------------------------------
# MAIN FUNCTION
# --------------------------------------------------------------------------------------------------
def print_help() -> None:
"""Show help screen."""
print("Usage:", os.path.basename(__file__), "[options] [PHP-EXT]...")
print(" ", os.path.basename(__file__), "-h, --help")
print()
print("This script will generate the Ansible group_vars file: .ansible/group_vars/all/mods.yml")
print("based on all the modules found in php_modules/ directory.")
print()
print("Positional arguments:")
print(" [PHP-EXT] Specify None, one or more PHP extensions to generate group_vars for.")
print(" When no PHP extension is specified (argument is omitted), group_vars")
print(" for all extensions will be genrated.")
print(" When one or more PHP extension are specified, only group_vars for")
print(" these extensions will be created.")
print(" only be generated for this single module (and its dependencies).")
print(" This is useful if you want to test new modules and not build all")
print(" previous modules in the Dockerfile.")
print()
print(" Note: You still need to generate the Dockerfiles via Ansible for")
print(" the changes to take effect, before building the image.")
print("Optional arguments:")
print(" -i Ignore dependent modules.")
print(" By default each exentions is checked for build dependencies of other")
print(" extensions. For example many extensions build against libxml ext.")
print(" By specifying -i, those dependencies are not beeing added to")
print(" ansible group_vars. Use at your own risk.")
def main(argv: List[str]) -> None:
"""Main entrypoint."""
ignore_dependencies = False
selected_modules = []
if len(argv):
for arg in argv:
if arg in ("-h", "--help"):
print_help()
sys.exit(0)
for arg in argv:
if arg.startswith("-") and arg != "-i":
print("Invalid argument:", arg)
print("Use -h or --help for help")
sys.exit(1)
if arg == "-i":
ignore_dependencies = True
else:
selected_modules.append(arg)
# Get modules in order of dependencies
modules = get_modules(selected_modules, ignore_dependencies)
module_tree = get_module_dependency_tree(modules)
names = resolve_module_dependency_tree(module_tree)
print("#", "-" * 78)
print("# Paths")
print("#", "-" * 78)
print("Repository: ", REPOSITORY_PATH)
print("PHP Module: ", PHP_MODULE_PATH)
print("Group Vars: ", GROUP_VARS_PATH)
print()
print("#", "-" * 78)
print("# Module directories")
print("#", "-" * 78)
print_modules(modules)
print()
print("#", "-" * 78)
print("# Build Dependency Tree")
print("#", "-" * 78)
print_dependency_tree(module_tree)
print()
print("#", "-" * 78)
print("# Build order")
print("#", "-" * 78)
print("\n".join(names))
# Create group_vars file mods.yml
write_group_vars(names)
if __name__ == "__main__":
main(sys.argv[1:])