2016-01-25 12:52:18 +01:00
#!/usr/bin/env python3
2017-01-31 09:15:18 +01:00
# -*- coding: utf8 -*-
2016-01-25 12:52:18 +01:00
import sys
import os
2016-12-23 18:06:44 +01:00
import re
2016-01-25 12:52:18 +01:00
import json
2017-08-31 02:01:29 +02:00
import shlex
import urllib . request
import codecs
reader = codecs . getreader ( " utf-8 " )
return_code = 0
2016-01-25 12:52:18 +01:00
2016-11-03 19:09:07 +01:00
2016-01-25 12:52:18 +01:00
class c :
2016-11-03 19:09:07 +01:00
HEADER = ' \033 [95m '
OKBLUE = ' \033 [94m '
OKGREEN = ' \033 [92m '
WARNING = ' \033 [93m '
2017-08-31 02:01:29 +02:00
MAYBE_FAIL = ' \033 [96m '
2016-11-03 19:09:07 +01:00
FAIL = ' \033 [91m '
END = ' \033 [0m '
BOLD = ' \033 [1m '
UNDERLINE = ' \033 [4m '
2016-01-25 12:52:18 +01:00
def header ( app_path ) :
2016-11-03 19:09:07 +01:00
print ( c . UNDERLINE + c . HEADER + c . BOLD +
2016-12-19 07:09:32 +01:00
" 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 " ,
2018-12-02 17:54:54 +01:00
" Official helpers: https://yunohost.org/#/packaging_apps_helpers_en \n " ,
2018-05-26 20:42:14 +02:00
" Experimental helpers: https://github.com/YunoHost-Apps/Experimental_helpers \n "
2016-12-19 07:09:32 +01:00
" Checking " + c . BOLD + app_path + c . END + " package \n " )
2016-11-03 19:09:07 +01:00
2016-01-25 12:52:18 +01:00
def print_right ( str ) :
2016-11-03 19:09:07 +01:00
print ( c . OKGREEN + " ✔ " , str , c . END )
2016-01-25 12:52:18 +01:00
2017-08-31 02:01:29 +02:00
def print_warning ( str ) :
print ( c . WARNING + " ! " , str , c . END )
def print_wrong ( str , reliable = True ) :
if reliable :
global return_code
return_code = 1
print ( c . FAIL + " ✘ " , str , c . END )
else :
print ( c . MAYBE_FAIL + " ? " , str , c . END )
def urlopen ( url ) :
try :
conn = urllib . request . urlopen ( url )
except urllib . error . HTTPError as e :
return { ' content ' : ' ' , ' code ' : e . code }
except urllib . error . URLError as e :
print ( ' URLError ' )
return { ' content ' : conn . read ( ) . decode ( ' UTF8 ' ) , ' code ' : 200 }
2016-11-03 19:09:07 +01:00
2016-01-25 12:52:18 +01:00
def check_files_exist ( app_path ) :
2016-11-03 19:09:07 +01:00
"""
Check files exist
2017-04-11 09:44:13 +02:00
' backup ' and ' restore ' scripts are mandatory
2016-11-03 19:09:07 +01:00
"""
2016-12-23 18:21:38 +01:00
2017-04-11 10:10:29 +02:00
print ( c . BOLD + c . HEADER + " >>>> MISSING FILES <<<< " + c . END )
2016-12-19 06:53:22 +01:00
fnames = ( " manifest.json " , " scripts/install " , " scripts/remove " ,
2017-08-31 02:01:29 +02:00
" scripts/upgrade " , " scripts/backup " , " scripts/restore " , " LICENSE " ,
" README.md " )
2016-12-18 02:37:07 +01:00
2017-04-11 09:44:13 +02:00
for nbr , fname in enumerate ( fnames ) :
2017-08-31 02:01:29 +02:00
if not check_file_exist ( app_path + " / " + fname ) :
2017-04-11 09:44:13 +02:00
if nbr != 4 and nbr != 5 :
2017-08-31 02:01:29 +02:00
print_wrong ( fname )
else :
print_warning ( fname )
2016-11-03 19:09:07 +01:00
2016-01-25 12:52:18 +01:00
def check_file_exist ( file_path ) :
2016-11-03 19:09:07 +01:00
return 1 if os . path . isfile ( file_path ) and os . stat ( file_path ) . st_size > 0 else 0
2016-01-25 12:52:18 +01:00
def read_file ( file_path ) :
2017-08-31 02:01:29 +02:00
f = open ( file_path )
# remove every comments and empty lines from the file content to avoid
# false positives
file = shlex . shlex ( f , False )
#file = filter(None, re.sub("#.*[^\n]", "", f.read()).splitlines())
2016-11-03 19:09:07 +01:00
return file
2016-01-25 12:52:18 +01:00
2017-02-04 08:48:57 +01:00
def check_source_management ( app_path ) :
2017-04-11 10:10:29 +02:00
print ( c . BOLD + c . HEADER + " \n >>>> SOURCES MANAGEMENT <<<< " + c . END )
2017-02-07 19:33:21 +01:00
DIR = os . path . join ( app_path , " sources " )
# Check if there is more than six files on 'sources' folder
2017-02-09 17:59:47 +01:00
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 :
2017-08-31 02:01:29 +02:00
print_warning ( " [YEP-3.3] 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 ' ynh_setup_source ' . \n See "
" https://dev.yunohost.org/issues/201#Conclusion-chart " )
def is_license_mention_in_readme ( path ) :
readme_path = os . path . join ( path , ' README.md ' )
if os . path . isfile ( readme_path ) :
return " LICENSE " in open ( readme_path ) . read ( )
return False
def check_manifest ( path ) :
manifest = os . path . join ( path , ' manifest.json ' )
if not os . path . exists ( manifest ) :
return
2017-04-11 10:10:29 +02:00
print ( c . BOLD + c . HEADER + " \n >>>> MANIFEST <<<< " + c . END )
2016-11-03 19:09:07 +01:00
"""
2016-12-18 02:37:07 +01:00
Check if there is no comma syntax issue
"""
2016-11-03 19:09:07 +01:00
try :
with open ( manifest , encoding = ' utf-8 ' ) as data_file :
manifest = json . loads ( data_file . read ( ) )
except :
2017-08-31 02:01:29 +02:00
print_wrong ( " [YEP-2.1] Syntax (comma) or encoding issue with manifest.json. "
" Can ' t check file. " )
2016-12-18 02:37:07 +01:00
2017-08-31 02:01:29 +02:00
fields = ( " name " , " id " , " packaging_format " , " description " , " url " , " version " ,
" license " , " maintainer " , " requirements " , " multi_instance " ,
" services " , " arguments " )
2016-12-18 02:37:07 +01:00
2016-12-19 06:53:22 +01:00
for field in fields :
2017-08-31 02:01:29 +02:00
if field not in manifest :
print_warning ( " [YEP-2.1] \" " + field + " \" field is missing " )
2016-12-18 02:37:07 +01:00
2016-11-03 19:09:07 +01:00
"""
2016-11-25 18:05:13 +01:00
Check values in keys
"""
2016-12-18 02:37:07 +01:00
2016-11-03 19:09:07 +01:00
if " packaging_format " not in manifest :
2017-08-31 02:01:29 +02:00
print_wrong ( " [YEP-2.1] \" packaging_format \" key is missing " )
elif not isinstance ( manifest [ " packaging_format " ] , int ) :
print_wrong ( " [YEP-2.1] \" packaging_format \" : value isn ' t an integer type " )
elif manifest [ " packaging_format " ] != 1 :
print_wrong ( " [YEP-2.1] \" packaging_format \" field: current format value is ' 1 ' " )
# YEP 1.1 Name is app
if " id " in manifest :
if not re . match ( ' ^[a-z1-9]((_|-)?[a-z1-9])+$ ' , manifest [ " id " ] ) :
print_wrong ( " [YEP-1.1] ' id ' field ' %s ' should respect this regex "
" ' ^[a-z1-9]((_|-)?[a-z1-9])+$ ' " )
if " name " in manifest :
if len ( manifest [ " name " ] ) > 22 :
print_warning ( " [YEP-1.1] The ' name ' field shouldn ' t be too long to be "
" able to be with one line in the app list. The most "
" current bigger name is actually compound of 22 characters. " )
# YEP 1.2 Put the app in a weel known repo
if " id " in manifest :
official_list_url = " https://raw.githubusercontent.com/YunoHost/apps/master/official.json "
official_list = json . loads ( urlopen ( official_list_url ) [ ' content ' ] )
community_list_url = " https://raw.githubusercontent.com/YunoHost/apps/master/community.json "
community_list = json . loads ( urlopen ( community_list_url ) [ ' content ' ] )
if manifest [ " id " ] not in official_list and manifest [ " id " ] not in community_list :
print_warning ( " [YEP-1.2] This app is not registered in official or community applications " )
# YEP 1.3 License
if " license " in manifest :
for license in manifest [ ' license ' ] . replace ( ' & ' , ' , ' ) . split ( ' , ' ) :
code_license = ' <code property= " spdx:licenseId " > ' + license + ' </code> '
link = " https://spdx.org/licenses/ "
if license == " nonfree " :
print_warning ( " [YEP-1.3] The correct value for non free license in license "
" field is ' non-free ' and not ' nonfree ' " )
license = " non-free "
if license in [ " free " , " non-free " , " dep-non-free " ] :
if not is_license_mention_in_readme ( path ) :
print_warning ( " [YEP-1.3] The use of ' %s ' in license field implies to "
" write something about the license in your "
" README.md " % ( license ) )
2017-11-14 22:11:09 +01:00
if license in [ " non-free " , " dep-non-free " ] :
print_warning ( " [YEP-1.3] ' non-free ' apps can ' t be officialized. "
" Their integration is still being discussed, "
" especially for apps with non-free dependencies " )
2017-08-31 02:01:29 +02:00
elif code_license not in urlopen ( link ) [ ' content ' ] :
print_warning ( " [YEP-1.3] The license ' %s ' is not registered in "
2018-01-16 15:20:22 +01:00
" https://spdx.org/licenses/ . It can be a typo error. "
" If not, you should replace it by ' free ' or ' non-free ' "
" and give some explanations in the README.md. " % ( license ) )
2017-08-31 02:01:29 +02:00
# YEP 1.4 Inform if we continue to maintain the app
# YEP 1.5 Update regularly the app status
# YEP 1.6 Check regularly the evolution of the upstream
# YEP 1.7 - Add an app to the YunoHost-Apps organization
if " id " in manifest :
repo = " https://github.com/YunoHost-Apps/ %s _ynh " % ( manifest [ " id " ] )
is_not_added_to_org = urlopen ( repo ) [ ' code ' ] == 404
if is_not_added_to_org :
print_warning ( " [YEP-1.7] You should add your app in the "
" YunoHost-Apps organisation. " )
# YEP 1.8 Publish test request
# YEP 1.9 Document app
if " description " in manifest and " name " in manifest :
if manifest [ " description " ] == manifest [ " name " ] :
print_warning ( " [YEP-1.9] You should write a good description of the "
" app (1 line is enough). " )
#TODO test a specific template in README.md
# YEP 1.10 Garder un historique de version propre
# YEP 1.11 Cancelled
# YEP 2.1
2016-11-03 19:09:07 +01:00
if " multi_instance " in manifest and manifest [ " multi_instance " ] != 1 and manifest [ " multi_instance " ] != 0 :
print_wrong (
2017-08-31 02:01:29 +02:00
" [YEP-2.1] \" multi_instance \" field must be boolean type values ' true ' or ' false ' and not string type " )
2016-12-18 02:37:07 +01:00
2016-11-03 19:09:07 +01:00
if " services " in manifest :
services = ( " nginx " , " php5-fpm " , " mysql " , " uwsgi " , " metronome " ,
" postfix " , " dovecot " ) # , "rspamd", "rmilter")
2016-12-18 02:37:07 +01:00
2016-12-19 06:53:22 +01:00
for service in manifest [ " services " ] :
if service not in services :
2017-08-31 02:01:29 +02:00
print_warning ( " [YEP-2.1] " + service + " service may not exist " )
2016-12-18 02:37:07 +01:00
2016-11-03 19:09:07 +01:00
if " install " in manifest [ " arguments " ] :
types = ( " domain " , " path " , " password " , " user " , " admin " )
2016-12-18 02:37:07 +01:00
2016-12-19 06:53:22 +01:00
for nbr , typ in enumerate ( types ) :
for install_arg in manifest [ " arguments " ] [ " install " ] :
if typ == install_arg [ " name " ] :
if " type " not in install_arg :
2017-08-31 02:01:29 +02:00
print_wrong ( " [YEP-2.1] You should specify the type of the key with %s " % ( typ ) )
2016-12-23 18:21:38 +01:00
2016-11-03 19:09:07 +01:00
2016-01-25 12:52:18 +01:00
2017-01-28 10:47:09 +01:00
def check_script ( path , script_name , script_nbr ) :
2016-12-23 18:21:38 +01:00
2016-11-03 19:09:07 +01:00
script_path = path + " /scripts/ " + script_name
2016-12-18 02:37:07 +01:00
2016-11-03 19:09:07 +01:00
if check_file_exist ( script_path ) == 0 :
return
2016-12-18 02:37:07 +01:00
2017-04-11 10:10:29 +02:00
print ( c . BOLD + c . HEADER + " \n >>>> " ,
2016-12-18 02:36:58 +01:00
script_name . upper ( ) , " SCRIPT <<<< " + c . END )
2017-08-31 02:01:29 +02:00
check_non_helpers_usage ( read_file ( script_path ) )
2017-01-28 10:47:09 +01:00
if script_nbr < 5 :
2017-08-31 02:01:29 +02:00
check_verifications_done_before_modifying_system ( read_file ( script_path ) )
check_set_usage ( script_name , read_file ( script_path ) )
2018-12-13 20:47:49 +01:00
check_helper_usage_dependencies ( script_path , script_name )
check_helper_usage_unix ( script_path , script_name )
check_helper_consistency ( script_path , script_name )
2017-08-31 02:01:29 +02:00
#check_arg_retrieval(script.copy())
2016-11-03 19:09:07 +01:00
2016-01-25 12:52:18 +01:00
def check_verifications_done_before_modifying_system ( script ) :
2016-11-03 19:09:07 +01:00
"""
Check if verifications are done before modifying the system
"""
2017-08-31 02:01:29 +02:00
ok = True
modify_cmd = ' '
2016-12-19 06:53:22 +01:00
cmds = ( " cp " , " mkdir " , " rm " , " chown " , " chmod " , " apt-get " , " apt " , " service " ,
2017-08-31 02:01:29 +02:00
" find " , " sed " , " mysql " , " swapon " , " mount " , " dd " , " mkswap " , " useradd " )
cmds_before_exit = [ ]
is_exit = False
for cmd in script :
if " ynh_die " == cmd or " exit " == cmd :
is_exit = True
break
cmds_before_exit . append ( cmd )
2016-12-18 02:37:07 +01:00
2017-08-31 02:01:29 +02:00
if not is_exit :
return
2016-12-18 02:37:07 +01:00
2017-08-31 02:01:29 +02:00
for cmd in cmds_before_exit :
if " ynh_die " == cmd or " exit " == cmd :
break
if not ok or cmd in cmds :
modify_cmd = cmd
ok = False
2016-11-03 19:09:07 +01:00
break
2016-12-18 02:37:07 +01:00
if not ok :
2017-08-31 02:01:29 +02:00
print_wrong ( " [YEP-2.4] ' ynh_die ' or ' exit ' command is executed with system modification before (cmd ' %s ' ). \n "
" This system modification is an issue if a verification exit the script. \n "
" You should move this verification before any system modification. " % ( modify_cmd ) , False )
2016-01-25 12:52:18 +01:00
2017-04-11 10:10:29 +02:00
2016-11-25 16:14:49 +01:00
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 '
"""
2016-12-18 02:37:07 +01:00
ok = True
2017-08-31 02:01:29 +02:00
#TODO
#for line_nbr, cmd in script:
# if "yunohost app setting" in cmd:
# print_wrong("[YEP-2.11] Line {}: 'yunohost app setting' command is deprecated,"
# " please use helpers ynh_app_setting_(set,get,delete)."
# .format(line_nbr + 1))
# ok = False
2016-12-18 02:37:07 +01:00
2017-08-31 02:01:29 +02:00
if not ok :
print ( " Helpers documentation: "
" https://yunohost.org/#/packaging_apps_helpers \n "
" code: https://github.com/YunoHost/yunohost/…helpers " )
2016-12-23 18:21:38 +01:00
2017-08-31 02:01:29 +02:00
if " exit " in script :
print_wrong ( " [YEP-2.4] ' exit ' command shouldn ' t be used. "
" Use ' ynh_die ' helper instead. " )
2016-11-25 16:14:49 +01:00
2017-01-28 11:52:22 +01:00
def check_set_usage ( script_name , script ) :
present = False
2017-08-31 02:01:29 +02:00
if script_name in [ " backup " , " remove " ] :
present = " ynh_abort_if_errors " in script or " set -eu " in script
2017-01-28 11:52:22 +01:00
else :
2017-08-31 02:01:29 +02:00
present = " ynh_abort_if_errors " in script
if script_name == " remove " :
# Remove script shouldn't use set -eu or ynh_abort_if_errors
if present :
print_wrong ( " [YEP-2.4] set -eu or ynh_abort_if_errors is present. "
" If there is a crash it could put yunohost system in "
" invalidated states. For details, look at "
" https://dev.yunohost.org/issues/419 " )
else :
if not present :
print_wrong ( " [YEP-2.4] ynh_abort_if_errors is missing. For details, "
" look at https://dev.yunohost.org/issues/419 " )
2017-01-28 11:52:22 +01:00
2017-03-07 13:59:37 +01:00
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 .
"""
present = False
2017-08-31 02:01:29 +02:00
for cmd in script :
if cmd == ' $ ' and script . get_token ( ) in [ str ( x ) for x in range ( 1 , 10 ) ] :
present = True
2017-03-07 13:59:37 +01:00
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 " )
2018-12-13 20:47:49 +01:00
def check_helper_usage_dependencies ( path , script_name ) :
2018-05-26 20:27:24 +02:00
"""
2018-05-27 12:01:38 +02:00
Detect usage of ynh_package_ * & apt - get *
and suggest herlpers ynh_install_app_dependencies and ynh_remove_app_dependencies
2018-05-26 20:27:24 +02:00
"""
2018-12-13 20:47:49 +01:00
script = read_file ( path )
2018-05-26 20:27:24 +02:00
2018-05-27 12:01:38 +02:00
if " ynh_package_install " in script or " apt-get install " in script :
print_warning ( " You should not use `ynh_package_install` or `apt-get install`, use `ynh_install_app_dependencies` instead " )
2018-05-26 20:27:24 +02:00
2018-05-27 12:01:38 +02:00
if " ynh_package_remove " in script or " apt-get remove " in script :
print_warning ( " You should not use `ynh_package_remove` or `apt-get removeè, use `ynh_remove_app_dependencies` instead " )
2017-03-07 13:59:37 +01:00
2018-12-13 20:47:49 +01:00
def check_helper_usage_unix ( path , script_name ) :
2018-05-26 20:40:27 +02:00
"""
2018-05-27 12:01:38 +02:00
Detect usage of unix commands with helper equivalents :
2018-12-02 17:59:45 +01:00
- sudo → ynh_exec_as
2018-05-27 12:01:38 +02:00
- rm → ynh_secure_remove
- sed - i → ynh_replace_string
2018-05-26 20:40:27 +02:00
"""
2018-12-13 20:47:49 +01:00
script = read_file ( path )
2018-05-26 20:40:27 +02:00
if " rm -rf " in script :
2018-12-02 17:59:45 +01:00
print_warning ( " You should avoid using `rm -rf`, please use `ynh_secure_remove` instead " )
2018-05-26 20:40:27 +02:00
if " sed -i " in script :
2018-12-02 17:59:45 +01:00
print_warning ( " You should avoid using `sed -i`, please use `ynh_replace_string` instead " )
2018-05-26 20:40:27 +02:00
if " sudo " in script :
2018-12-02 17:59:45 +01:00
print_warning ( " You should not need to use `sudo`, the script is being run as root. (If you need to run a command using a specific user, use `ynh_exec_as`) " )
2018-05-26 20:40:27 +02:00
2018-12-13 20:47:49 +01:00
def check_helper_consistency ( path , script_name ) :
2018-05-27 11:54:28 +02:00
"""
check if ynh_install_app_dependencies is present in install / upgrade / restore
so dependencies are up to date after restoration or upgrade
"""
2018-12-13 20:47:49 +01:00
script = read_file ( path )
2018-05-27 11:54:28 +02:00
if script_name == " install " and " ynh_install_app_dependencies " in script :
for name in [ " upgrade " , " restore " ] :
try :
2018-12-13 20:47:49 +01:00
script2 = read_file ( os . path . dirname ( path ) + " / " + name )
2018-05-27 11:54:28 +02:00
if not " ynh_install_app_dependencies " in script2 :
print_warning ( " ynh_install_app_dependencies should also be in %s script " % name )
except FileNotFoundError :
pass
2016-01-25 12:52:18 +01:00
if __name__ == ' __main__ ' :
2016-11-03 19:09:07 +01:00
if len ( sys . argv ) != 2 :
print ( " Give one app package path. " )
exit ( )
2016-12-18 02:37:07 +01:00
2016-12-23 18:21:38 +01:00
# "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
2016-11-03 19:09:07 +01:00
app_path = sys . argv [ 1 ]
header ( app_path )
2017-08-31 02:01:29 +02:00
check_files_exist ( app_path )
check_source_management ( app_path )
check_manifest ( app_path ) # + "/manifest.json")
2016-12-18 02:37:07 +01:00
scripts = [ " install " , " remove " , " upgrade " , " backup " , " restore " ]
2016-11-03 19:42:00 +01:00
for ( dirpath , dirnames , filenames ) in os . walk ( os . path . join ( app_path , " scripts " ) ) :
for filename in filenames :
2016-11-05 20:09:10 +01:00
if filename not in scripts and filename [ - 4 : ] != " .swp " :
2016-11-03 19:42:00 +01:00
scripts . append ( filename )
2016-12-18 02:37:07 +01:00
2017-01-28 10:47:09 +01:00
for script_nbr , script in enumerate ( scripts ) :
2017-08-31 02:01:29 +02:00
check_script ( app_path , script , script_nbr )
2016-12-23 18:21:38 +01:00
sys . exit ( return_code )