yunohost/doc/generate_helper_doc.py
2024-06-24 20:38:46 +00:00

283 lines
9.6 KiB
Python

#!/usr/env/python3
import sys
import os
import glob
import datetime
import subprocess
tree = {
"sources": {
"title": "Sources",
"notes": "This is coupled to the 'sources' resource in the manifest.toml",
"subsections": ["sources"],
},
"tech": {
"title": "App technologies",
"notes": "These allow to install specific version of the technology required to run some apps",
"subsections": ["nodejs", "ruby", "go", "composer"],
},
"db": {
"title": "Databases",
"notes": "This is coupled to the 'database' resource in the manifest.toml - at least for mysql/postgresql. Mongodb/redis may have better integration in the future.",
"subsections": ["mysql", "postgresql", "mongodb", "redis"],
},
"conf": {
"title": "Configurations / templating",
"subsections": [
"templating",
"nginx",
"php",
"systemd",
"fail2ban",
"logrotate",
],
},
"misc": {
"title": "Misc tools",
"subsections": [
"utils",
"setting",
"string",
"backup",
"logging",
"multimedia",
],
},
"meh": {
"title": "Deprecated or handled by the core / app resources since v2",
"subsections": ["permission", "apt", "systemuser"],
},
}
def get_current_commit():
p = subprocess.Popen(
"git rev-parse --verify HEAD",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
current_commit = stdout.strip().decode("utf-8")
return current_commit
def render(tree, helpers_version):
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import get_styles
conv = Ansi2HTMLConverter()
shell_css = "\n".join(map(str, get_styles(conv.dark_bg)))
def shell_to_html(shell):
return conv.convert(shell, False)
template = open("helper_doc_template.md", "r").read()
t = Template(template)
t.globals["now"] = datetime.datetime.utcnow
result = t.render(
tree=tree,
date=datetime.datetime.now().strftime("%d/%m/%Y"),
version=open("../debian/changelog").readlines()[0].split()[1].strip("()"),
helpers_version=helpers_version,
current_commit=get_current_commit(),
convert=shell_to_html,
shell_css=shell_css,
)
open(f"helpers.v{helpers_version}.md", "w").write(result)
##############################################################################
class Parser:
def __init__(self, filename):
self.filename = filename
self.file = open(filename, "r").readlines()
self.blocks = None
def parse_blocks(self):
self.blocks = []
current_reading = "void"
current_block = {"name": None, "line": -1, "comments": [], "code": []}
for i, line in enumerate(self.file):
if line.startswith("#!/bin/bash"):
continue
line = line.rstrip().replace("\t", " ")
if current_reading == "void":
if is_global_comment(line):
# We start a new comment bloc
current_reading = "comments"
assert line.startswith("# ") or line == "#", malformed_error(i)
current_block["comments"].append(line[2:])
else:
pass
# assert line == "", malformed_error(i)
continue
elif current_reading == "comments":
if is_global_comment(line):
# We're still in a comment bloc
assert line.startswith("# ") or line == "#", malformed_error(i)
current_block["comments"].append(line[2:])
elif line.strip() == "" or line.startswith("_ynh"):
# Well eh that was not an actual helper definition ... start over ?
current_reading = "void"
current_block = {
"name": None,
"line": -1,
"comments": [],
"code": [],
}
elif not (line.endswith("{") or line.endswith("()")):
# Well we're not actually entering a function yet eh
# (c.f. global vars)
pass
else:
# We're getting out of a comment bloc, we should find
# the name of the function
assert len(line.split()) >= 1, "Malformed line {} in {}".format(
i,
self.filename,
)
current_block["line"] = i
current_block["name"] = line.split()[0].strip("(){")
# Then we expect to read the function
current_reading = "code"
elif current_reading == "code":
if line == "}":
# We're getting out of the function
current_reading = "void"
# Then we keep this bloc and start a new one
# (we ignore helpers containing [internal] ...)
if (
"[packagingv1]" not in current_block["comments"]
and not any(
line.startswith("[internal]")
for line in current_block["comments"]
)
and not current_block["name"].startswith("_")
):
self.blocks.append(current_block)
current_block = {
"name": None,
"line": -1,
"comments": [],
"code": [],
}
else:
current_block["code"].append(line)
continue
def parse_block(self, b):
b["brief"] = ""
b["details"] = ""
b["usage"] = ""
b["args"] = []
b["ret"] = ""
subblocks = "\n".join(b["comments"]).split("\n\n")
for i, subblock in enumerate(subblocks):
subblock = subblock.strip()
if i == 0:
b["brief"] = subblock
continue
elif subblock.startswith("example:"):
b["example"] = " ".join(subblock.split()[1:])
continue
elif subblock.startswith("examples:"):
b["examples"] = subblock.split("\n")[1:]
continue
elif subblock.startswith("usage"):
for line in subblock.split("\n"):
if line.startswith("| arg"):
linesplit = line.split()
argname = linesplit[2]
# Detect that there's a long argument version (-f, --foo - Some description)
if argname.endswith(",") and linesplit[3].startswith("--"):
argname = argname.strip(",")
arglongname = linesplit[3]
argdescr = " ".join(linesplit[5:])
b["args"].append((argname, arglongname, argdescr))
else:
argdescr = " ".join(linesplit[4:])
b["args"].append((argname, argdescr))
elif line.startswith("| ret"):
b["ret"] = " ".join(line.split()[2:])
else:
if line.startswith("usage"):
line = " ".join(line.split()[1:])
b["usage"] += line + "\n"
continue
elif subblock.startswith("| arg"):
for line in subblock.split("\n"):
if line.startswith("| arg"):
linesplit = line.split()
argname = linesplit[2]
# Detect that there's a long argument version (-f, --foo - Some description)
if argname.endswith(",") and linesplit[3].startswith("--"):
argname = argname.strip(",")
arglongname = linesplit[3]
argdescr = " ".join(linesplit[5:])
b["args"].append((argname, arglongname, argdescr))
else:
argdescr = " ".join(linesplit[4:])
b["args"].append((argname, argdescr))
continue
else:
b["details"] += subblock + "\n\n"
b["usage"] = b["usage"].strip()
def is_global_comment(line):
return line.startswith("#")
def malformed_error(line_number):
return "Malformed file line {} ?".format(line_number)
def main():
if len(sys.argv) == 1:
print("This script needs the helper version (1, 2, 2.1) as an argument")
sys.exit(1)
version = sys.argv[1]
for section in tree.values():
section["helpers"] = {}
for subsection in section["subsections"]:
print(f"Parsing {subsection} ...")
helper_file = f"../helpers/helpers.v{version}.d/{subsection}"
assert os.path.isfile(helper_file), f"Uhoh, {file} doesn't exists?"
p = Parser(helper_file)
p.parse_blocks()
for b in p.blocks:
p.parse_block(b)
section["helpers"][subsection] = p.blocks
render(tree, version)
main()