package_linter/package_linter.py

342 lines
11 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf8 -*-
import sys
import os
import re
import json
class c:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
END = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def header(app_path):
print(c.UNDERLINE + c.HEADER + c.BOLD +
"YUNOHOST APP PACKAGE LINTER\n", c.END,
"App packaging documentation: https://yunohost.org/#/packaging_apps\n",
"App package example: https://github.com/YunoHost/example_ynh\n",
"Checking " + c.BOLD + app_path + c.END + " package\n")
def print_right(str):
print(c.OKGREEN + "", str, c.END)
def print_wrong(str):
print(c.FAIL + "", str, c.END)
def check_files_exist(app_path):
"""
Check files exist
"""
return_code = 0
print (c.BOLD + c.HEADER + ">>>> MISSING FILES <<<<" + c.END)
fnames = ("manifest.json", "scripts/install", "scripts/remove",
"scripts/upgrade", "scripts/backup", "scripts/restore", "LICENSE", "README.md")
for fname in fnames:
if check_file_exist(app_path + "/" + fname):
print_right(fname)
else:
print_wrong(fname)
return_code = 1
return return_code
def check_file_exist(file_path):
return 1 if os.path.isfile(file_path) and os.stat(file_path).st_size > 0 else 0
def read_file(file_path):
with open(file_path) as f:
# remove every comments from the file content to avoid false positives
file = re.sub("#.*[^\n]", "", f.read()).splitlines()
return file
def check_source_management(app_path):
print (c.BOLD + c.HEADER + "\n>>>> SOURCES MANAGEMENT <<<<" + c.END)
DIR = os.path.join(app_path, "sources")
# Check if there is more than six files on 'sources' folder
if os.path.exists(os.path.join(app_path, "sources")) and \
len([name for name in os.listdir(DIR) if os.path.isfile(os.path.join(DIR, name))]) > 5:
print_wrong("Upstream app sources shouldn't be stored on this 'sources' folder of this git repository as a copy/paste.\n" +
"At installation, the package should download sources from upstream via 'wget', git submodule or git subtree.\n" +
"See https://dev.yunohost.org/issues/201#Conclusion-chart")
else:
print_right("Upstream app sources do not seems to be stored on the git repository as a copy/paste")
def check_manifest(manifest):
print (c.BOLD + c.HEADER + "\n>>>> MANIFEST <<<<" + c.END)
"""
Check if there is no comma syntax issue
"""
return_code = 0
try:
with open(manifest, encoding='utf-8') as data_file:
manifest = json.loads(data_file.read())
print_right("Manifest syntax is good.")
except:
print_wrong(
"Syntax (comma) or encoding issue with manifest.json. Can't check file.")
return 1
fields = ("name", "id", "packaging_format", "description", "url",
"license", "maintainer", "requirements", "multi_instance", "services", "arguments")
for field in fields:
if field in manifest:
print_right("\"" + field + "\" field is present")
else:
print_wrong("\"" + field + "\" field is missing")
return_code = 1
"""
Check values in keys
"""
pf = 1
if "packaging_format" not in manifest:
print_wrong("\"packaging_format\" key is missing")
return_code = 1
pf = 0
if pf == 1 and isinstance(manifest["packaging_format"], int) != 1:
print_wrong("\"packaging_format\": value isn't an integer type")
return_code = 1
pf = 0
if pf == 1 and manifest["packaging_format"] != 1:
print_wrong("\"packaging_format\" field: current format value is '1'")
return_code = 1
pf = 0
if pf == 1:
print_right("\"packaging_format\" field is good")
"""
if "license" in manifest and manifest["license"] != "free" and manifest["license"] != "non-free":
print_wrong(
"You should specify 'free' or 'non-free' software package in the license field.")
return_code = 1
elif "license" in manifest:
print_right("\"licence\" key value is good")
"""
if "multi_instance" in manifest and manifest["multi_instance"] != 1 and manifest["multi_instance"] != 0:
print_wrong(
"\"multi_instance\" field must be boolean type values 'true' or 'false' and not string type")
return_code = 1
elif "multi_instance" in manifest:
print_right("\"multi_instance\" field is good")
if "services" in manifest:
services = ("nginx", "php5-fpm", "mysql", "uwsgi", "metronome",
"postfix", "dovecot") # , "rspamd", "rmilter")
for service in manifest["services"]:
if service not in services:
print_wrong(service + " service doesn't exist")
return_code = 1
if "install" in manifest["arguments"]:
types = ("domain", "path", "password", "user", "admin")
for nbr, typ in enumerate(types):
for install_arg in manifest["arguments"]["install"]:
if typ == install_arg["name"]:
if "type" not in install_arg:
print("You should specify the type of the key with", end=" ")
print(types[nbr - 1]) if nbr == 4 else print(typ)
return_code = 1
return return_code
def check_script(path, script_name, script_nbr):
return_code = 0
script_path = path + "/scripts/" + script_name
if check_file_exist(script_path) == 0:
return
print (c.BOLD + c.HEADER + "\n>>>>",
script_name.upper(), "SCRIPT <<<<" + c.END)
script = read_file(script_path)
return_code = check_non_helpers_usage(script) or return_code
if script_nbr < 5:
return_code = check_verifications_done_before_modifying_system(script) or return_code
return_code = check_set_usage(script_name, script) or return_code
check_arg_retrieval(script)
return return_code
def check_verifications_done_before_modifying_system(script):
"""
Check if verifications are done before modifying the system
"""
ex = 0
for line_number, line in enumerate(script):
if "ynh_die" in line or "exit " in line:
ex = line_number
cmds = ("cp", "mkdir", "rm", "chown", "chmod", "apt-get", "apt", "service",
"find", "sed", "mysql", "swapon", "mount", "dd", "mkswap", "useradd") # "yunohost"
ok = True
for line_nbr, line in enumerate(script):
if line_nbr >= ex:
break
for cmd in cmds:
if cmd in line and line[0] != '#':
ok = False
if not ok:
print(c.FAIL + "✘ At line", ex + 1,
"'ynh_die' or 'exit' command is executed with system modification before.\n",
"This system modification is an issue if a verification exit the script.\n",
"You should move this verification before any system modification." + c.END)
return 1
else:
print_right(
"Verifications (with 'ynh_die' or 'exit' commands) are done before any system modification.")
return 0
def check_non_helpers_usage(script):
"""
check if deprecated commands are used and propose helpers:
- 'yunohost app setting' > ynh_app_setting_(set,get,delete)
- 'exit' > 'ynh_die'
"""
return_code = 0
ok = True
for line_nbr, line in enumerate(script):
if "yunohost app setting" in line:
print_wrong("Line {}: 'yunohost app setting' command is deprecated, please use helpers ynh_app_setting_(set,get,delete).".format(line_nbr + 1))
ok = False
if ok:
print_right("Only helpers are used")
else:
print_wrong("Helpers documentation: https://yunohost.org/#/packaging_apps_helpers\ncode: https://github.com/YunoHost/yunohost/…helpers")
return_code = 1
ok = True
for line_nbr, line in enumerate(script):
if "exit " in line:
print_wrong("Line {}: 'exit' command shouldn't be used. Use 'ynh_die' helper instead.".format(line_nbr + 1))
ok = False
if ok:
print_right("no 'exit' command found: 'ynh_die' helper is possibly used")
else:
return_code = 1
return return_code
def check_set_usage(script_name, script):
return_code = 0
present = False
set_val = "set -u" if script_name == "remove" else "set -eu"
for line_nbr, line in enumerate(script):
if set_val in line:
present = True
break
if line_nbr > 5:
break
if present:
print_right(set_val + " is present at beginning of file")
else:
print_wrong(set_val + " is missing at beginning of file. For details, look at https://dev.yunohost.org/issues/419")
return_code = 1
return return_code
def check_arg_retrieval(script):
"""
Check arguments retrival from manifest is done with env var $YNH_APP_ARG_* and not with arg $1
env var was found till line ~30 on scripts. Stop file checking at L30: This could avoid wrong positives
Check only from '$1' to '$10' as 10 arg retrieval is already a lot.
"""
return_code = 0
present = False
exitFlag = False
for line_nbr, line in enumerate(script):
for i in range(1, 10):
if "$" + str(i) in line:
print_wrong("At line {}: \"{}\"".format(line_nbr, line))
present = True
if line_nbr > 30:
exitFlag = True
break
if exitFlag == True:
break
if present:
print_wrong("Argument retrieval from manifest with $1 is deprecated. You may use $YNH_APP_ARG_*")
print_wrong("For more details see: https://yunohost.org/#/packaging_apps_arguments_management_en")
return_code = 1
else:
print_right("Argument retrieval from manifest seems to be done with environement variables")
return return_code
if __name__ == '__main__':
os.system("clear")
return_code = 0
if len(sys.argv) != 2:
print("Give one app package path.")
exit()
# "or" trick to always be 1 if 1 is present:
# 1 or 0 = 1
# 1 or 1 = 1
# 0 or 1 = 1
# 0 or 0 = 0
app_path = sys.argv[1]
header(app_path)
return_code = check_files_exist(app_path) or return_code
return_code = check_source_management(app_path) or return_code
return_code = check_manifest(app_path + "/manifest.json") or return_code
scripts = ["install", "remove", "upgrade", "backup", "restore"]
for (dirpath, dirnames, filenames) in os.walk(os.path.join(app_path, "scripts")):
for filename in filenames:
if filename not in scripts and filename[-4:] != ".swp":
scripts.append(filename)
for script_nbr, script in enumerate(scripts):
return_code = check_script(app_path, script, script_nbr) or return_code
sys.exit(return_code)