dockerfiles.py (138 lines of code) (raw):
#!/usr/bin/env python3
"""
Generate JetBrains JetBrains/qodana-docker
Usage:
python dockerfiles.py /path/to/release_dir
"""
import argparse
import json
import logging
import os
import re
import sys
from typing import Any, Dict
from jinja2 import Environment, FileSystemLoader, Template, select_autoescape
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def parse_args() -> str:
"""
Parse command-line arguments and return the release directory path.
Returns:
str: The path to the release directory.
"""
parser = argparse.ArgumentParser(description="Generate Dockerfiles from base templates.")
parser.add_argument(
"release_dir",
help="Path to the release directory containing public.json and template files."
)
args = parser.parse_args()
return args.release_dir
def validate_release_dir(release_dir: str) -> None:
"""
Validate that the release directory exists and is a directory.
Args:
release_dir (str): The path to the release directory.
Raises:
SystemExit: If the directory does not exist.
"""
if not os.path.isdir(release_dir):
logger.error("Release directory '%s' doesn't exist.", release_dir)
sys.exit(1)
def load_variants(release_dir: str) -> Dict[str, Any]:
"""
Load variant definitions from public.json in the release directory.
Args:
release_dir (str): The path to the release directory.
Returns:
Dict[str, Any]: A dictionary of variants from the JSON file.
Raises:
SystemExit: If the file is missing, cannot be read, or is invalid JSON.
"""
public_json_path = os.path.join(release_dir, "public.json")
if not os.path.isfile(public_json_path):
logger.error("'%s' not found.", public_json_path)
sys.exit(1)
try:
with open(public_json_path, "r", encoding="utf-8") as file:
variants = json.load(file)
return variants
except json.JSONDecodeError as e:
logger.error("Error decoding JSON from '%s': %s", public_json_path, e)
sys.exit(1)
except OSError as e:
logger.error("Error reading '%s': %s", public_json_path, e)
sys.exit(1)
def create_jinja_environment() -> Environment:
"""
Create and return a Jinja2 environment configured for file system loading and autoescaping.
Returns:
Environment: The configured Jinja2 environment.
"""
return Environment(
loader=FileSystemLoader("."),
autoescape=select_autoescape()
)
def load_template(env: Environment, template_path: str) -> Template:
"""
Load a Jinja2 template from the given path using the provided environment.
Args:
env (Environment): The Jinja2 environment to use.
template_path (str): The file path of the template.
Returns:
Template: The loaded Jinja2 template object.
Raises:
SystemExit: If the template cannot be loaded.
"""
try:
return env.get_template(template_path)
except Exception as e:
logger.error("Error loading template '%s': %s", template_path, e)
sys.exit(1)
def substitute_from_directives(content: str, base_dir: str) -> str:
"""
Recursively replace lines of the form:
FROM identifier
where 'identifier' is composed of letters and dashes, with the contents of
<identifier>.Dockerfile found in 'base_dir'.
Args:
content (str): The Dockerfile content to process.
base_dir (str): The directory containing base Dockerfiles to include.
Returns:
str: The processed Dockerfile content with all FROM directives substituted.
"""
pattern = re.compile(r"^(FROM)\s+([A-Za-z-]+)\s*$")
lines = content.splitlines()
new_lines = []
for line in lines:
match = pattern.match(line)
if match:
identifier = match.group(2)
file_path = os.path.join(base_dir, f"{identifier}.Dockerfile")
if os.path.isfile(file_path):
try:
with open(file_path, "r", encoding="utf-8") as inc_file:
included_content = inc_file.read()
# Recursively process the included file's content
substituted_content = substitute_from_directives(included_content, base_dir)
new_lines.append(substituted_content.rstrip())
except OSError as e:
logger.error("Error reading included file '%s': %s", file_path, e)
new_lines.append(line)
else:
# If no file found, leave the line as is.
new_lines.append(line)
else:
new_lines.append(line)
return "\n".join(new_lines)
def generate_variant_dockerfile(
variant: str,
data: Dict[str, Any],
base_dockerfile_dir: str,
intellij_template: Template,
thirdparty_template: Template,
release_dir: str
) -> str:
"""
Generate the final Dockerfile content for a specific variant.
Args:
variant (str): The name of the variant.
data (Dict[str, Any]): Variant-specific metadata from the JSON file.
base_dockerfile_dir (str): Path to the directory containing base Dockerfiles.
intellij_template (Template): Jinja2 template for IntelliJ-based variants.
thirdparty_template (Template): Jinja2 template for third-party variants.
release_dir (str): The main release directory path.
Returns:
str: The final Dockerfile content, or an empty string if an error occurred.
"""
base_source = data.get("from", variant)
base_dockerfile_path = os.path.join(base_dockerfile_dir, f"{base_source}.Dockerfile")
if not os.path.isfile(base_dockerfile_path):
logger.warning("Skipping %s: %s not found.", variant, base_dockerfile_path)
return ""
# Read and process the base Dockerfile content with recursive substitutions
try:
with open(base_dockerfile_path, "r", encoding="utf-8") as f:
base_content = f.read()
processed_base_content = substitute_from_directives(base_content, base_dockerfile_dir)
except OSError as e:
logger.error("Error processing base Dockerfile for variant '%s': %s", variant, e)
return ""
template = thirdparty_template if data.get("is_third_party", False) else intellij_template
snippet = template.render(
qd_release=release_dir,
qd_code=data.get("qd_code", ""),
description=data.get("description", ""),
variant=variant.split("-")[0],
qd_image=variant
)
final_dockerfile = processed_base_content.rstrip() + "\n\n" + snippet
return final_dockerfile
def write_dockerfile(variant: str, release_dir: str, dockerfile_content: str) -> None:
"""
Write the final Dockerfile content to the appropriate output directory.
Args:
variant (str): The variant name.
release_dir (str): The path to the release directory.
dockerfile_content (str): The complete Dockerfile content to write.
"""
if not dockerfile_content:
logger.debug("No Dockerfile content to write for variant '%s'. Skipping.", variant)
return
generated_disclaimer = "# This file was generated by https://github.com/JetBrains/qodana-docker/blob/main/dockerfiles.py. DO NOT EDIT MANUALLY."
dockerfile_content = f"{generated_disclaimer}\n\n{dockerfile_content}"
out_dir = os.path.join(release_dir, variant)
out_path = os.path.join(out_dir, "Dockerfile")
os.makedirs(out_dir, exist_ok=True)
try:
with open(out_path, "w", encoding="utf-8") as out_file:
out_file.write(dockerfile_content)
logger.info("Generated %s.", out_path)
except OSError as e:
logger.error("Error writing output for variant '%s': %s", variant, e)
def main() -> None:
"""
Main entry point: parse arguments, load variants, load templates, and generate Dockerfiles.
"""
release_dir = parse_args()
validate_release_dir(release_dir)
variants = load_variants(release_dir)
env = create_jinja_environment()
intellij_template_path = os.path.join(release_dir, "base", "templates", "intellij.Dockerfile.j2")
thirdparty_template_path = os.path.join(release_dir, "base", "templates", "thirdparty.Dockerfile.j2")
intellij_template = load_template(env, intellij_template_path)
thirdparty_template = load_template(env, thirdparty_template_path)
base_dockerfile_dir = os.path.join(release_dir, "base")
for variant, data in variants.items():
dockerfile_content = generate_variant_dockerfile(
variant,
data,
base_dockerfile_dir,
intellij_template,
thirdparty_template,
release_dir
)
write_dockerfile(variant, release_dir, dockerfile_content)
if __name__ == "__main__":
main()