Jacques Foucry bio photo

Jacques Foucry

IT, râleur et faiseur de pain

Email Twitter



Le quatrième article sur la compilaton automatique iOS et la distribution OTA

Résumé des épisodes précédents

Nous avons un script python qui permet de compiler un projet iOS et de mettre à disposition l’application ainsi compilée pour une distribution OTA (Over The Air).

Nous avons aussi remarqué que le mot de passe de la keychain était passé en paramètre et qu’en faisant une commande ps il est possible de récupérer ce mot de passe.

Une solution envisageable est l’utilisation d’un fichier de configuration. Celui-ci aurait des droits d’accès très restreint (600).

Réflexion sur les paramètres

Comment les paramètres doivent-ils être utilisés ? L’ordre de préférence que j’ai choisi est le suivant (vous pouvez avoir votre propre idée, mais c’est à vous de coder la solution) :

  • les paramètres de la ligne de commande prévalent sur ceux du fichier de configuration 
  • les paramètres du fichier de configuration prévalent sur les valeurs par défaut.

Autrement dit, si un paramètre est présent dans la ligne de commande et dans le fichier de configuration, celui du fichier sera ignoré au profit de celui de la ligne de commande.

Modification du script

Pour pouvoir accueillir nos futures modifications, il est nécessaire de revoir la façon dont le script est architecturé. Nous allons ajouter des fonctions et déplacer certains blocs de code. Ainsi la gestion du logger doit se faire très rapidement, juste après la lecture des paramètres de la ligne de commande.

Définir les fonctions

Vous le savez il est nécessaire de d’implémenter les fonctions avant de les utiliser. Nous allons donc factoriser au maximum notre code en mettant dans des fonctions une grande partie de celui-ci.

La liste des fonctions est la suivante :

  • writeOnSTDERR
  • checkPresence
  • checkParameter
  • openKeychain
  • compileApp
  • createIPA
  • retreiveInfo
  • createManifest
  • fillHTML
  • createIndexHTML
  • createTargetFolder
  • distribution
  • gitClone
  • gitPull

Certaines existent déjà, d’autres sont nouvelles. Par exemple checkParameter permet de vérifier l’existence d’un paramètre dans le fichier de configuration. En voici le code 

1
2
3
4
5
6
7
def checkParameter(parameter):
  	logger.debug("checkParameter: parameter = %s"% parameter)
  	try:
  	 	value = config.get("xcodebuild","%s" % parameter)
  	except ConfigParser.NoOptionError:
  	 	logger.debug("%s not found in config file" % parameter)
  	 	value=None

Explication de texte

On recherche de la valeur dans l’objet config, dans la section xcodebuild du paramètre
si l’option n’est pas trouvé on s’assure de mettre la valeur à None

Cette fonction peu vous paraitre peu claire, mais au fil de la lecture de cet article vous verrez la Lumière.

La gestion des arguments

Une fois nos fonctions déclarées et implémentées, nous allons passer à la gestion des paramètres. Elle est un peu différente de ce que nous avions précédemment. Puisque certains des paramètres peuvent se trouver dans un fichier de configuration, nous n’avons plus de paramètres requis. Et nous devons ajouter une option pour indiquer le fichier de configuration à utiliser.

1
  parser.add_argument('-C', '--configFile', action="store", dest="configFile", help="Configuration file, instead of each parameter")

La gestion des traces

Pas d’innovation ici.

La gestion du fichier de configuration

Notre fichier de configuration est très simple. Nous allons utiliser un nouveau module pour le gérer, ConfigParser. N’oubliez pas de l’ajouter dans les imports.

Ce module lit un fichier texte donné en paramètre. Ce fichier peut être diviser en section, identifiées par un titre de section entre crochet :

1
  [section]

Nous n’aurons besoin que d’une section dans notre fichier, nous l’appellerons [xcodebuild]. Mais nous reviendront sur le fichier de paramètre plus loin.

Nous avons besoin de récupérer le nom du fichier de configuration (en fait cela se fait comme avec tous les autres paramètres) :

1
  configFile = args.configFile

Et nous devons ensuite vérifier la présence du fichier de configuration et instancier un objet config qui va nous permettre d’en lire le contenu :

1
2
3
4
5
  if configFile is not None:
    if checkPresence(configFile) is not 1:
  	 config = ConfigParser.RawConfigParser()
  	 config.read("%s" % configFile)
  	 configFilePresent = True

La gestion des arguments (bis)

Nous avons vu plus haut comment lire les arguments passés à la ligne de commande. Par rapport à la version précédente du script, nous n’avons plus de paramètre requis. C’était fort utile puisque nous n’avions rien à faire pour les gérer. Si nous n’avons plus de paramètre marqué comme requis, il n’en demeure pas moins que certains sont absolument nécessaire à la bonne marche de notre script. C’est donc à nous qu incombe cette tâche.

Commençons par définir deux variables qui vont nous aider à compter le nombre d’erreur et à donner à l’utilisateur un message utile.

1
2
  numberOfError = 
  errorString=""

Tous les paramètres requis seront traités sur le même modèle (il doit être possible de factoriser le code dans une fonction, mais je ne m’en suis pas encore préoccupé).

1
2
3
4
5
6
7
8
9
10
11
12
  # Required
  keychain = args.keychain
  if keychain is None:
    	logger.debug("keychain is None")
    	if configFilePresent is True:
    	 	logger.debug("configFilePresent is True")
    	 	keychain = checkParameter("keychain")
    	 	logger.debug("Keychain value is: %s" % keychain)
    	if keychain is None:
    	 	logger.debug("keychain is None")
    	 	errorString = "Keychain"
    	 	++numbeOfError

Explication de texte :

On récupère le paramétre (keychain = args.keychain) ;
si le paramètre n’existe pas (if keychain is None:) ;
si le fichier de configuration est présent (if configFilePresent is True:) ;
nous allons chercher le paramètre dans le fichier de configuration à l’aide de la fonction obscure tout à l’heure chekcParameter
si le paramètre n’existe pas (c’est à dire qu’il n’est pas défini sur la ligne de commande ET qu’il n’a pas été trouvé dans le fichier de configuration)
nous avons un problème et nous mettons à jour les variables d’erreur, le compteur numberOfError et le texte d’accompagnement errorString

Simple non ?

Les autres paramètres (ceux qui ne sont pas obligatoire) se gèrent quasiment de la même façon sauf que nous n’avons pas à vérifier leur valeur après le retour de la fonction checkParameter 

1
2
3
4
5
6
  # username is not mandatory
  username = args.username
  if username is None:
    	if configFilePresent is True:
    	 	username = checkParameter("username")
    	 	logger.debug("username is: %s" % username)

La suite du script

La suite est relativement simple. Nous allons définir certaines variables en fonction d’autres (GSDK, workspace, etc). Ces variables était déjà présente dans la version précédente du script, leur définition est simplement déplacé.

On vérifie aussi le présence du dossier .xcodeproj.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  # --- Other variables
  logger.debug("project == %s"%(project))
  if 'xcodeproj' not in project:
    	logger.info("Project file path must end with xcodeproj")
    	writeOnSTDERR("Project file path must end with xcodeproj")
    	sys.exit(1)

  # -- GSDK is SDK without version (iphoneos5.0 -> iphoneos)
  GSDK = SDK[:-3]
  logger.debug("GDSK == %s"%(GSDK))
  # -- APPPath is the Path to the generated APP 
  APPPath="%s/build/%s-%s"%(workspace,configuration,GSDK)
  logger.debug("APPPath == %s"%(APPPath))

  # -- timestamp is used to create de unique target Folder
  timestamp = int(time.time())
  targetFolder="/tmp/%s-%d"%(target,timestamp)
  logger.debug("targetFolder == %s"%(targetFolder))

  logger.debug("project == %s"%(project))
  logger.debug("workspace == %s"%(workspace))

  # -- ProjectPath
  projectPath,projectName = os.path.split(project)

On vérifie aussi la présence de la keychain, du répertoire du projet s’il n’existe pas, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  # --- Check presence of the keychain and the project
  if checkPresence(keychain) != :
    	logger.debug("Keychain %s not found"%(keychain))
    	writeOnSTDERR("Keychain %s not found"%(keychain))
    	sys.exit(1)

  if checkPresence(projectPath) == 1:
    	logger.debug("%s does not exist"%(projectPath))
    	subprocess.call(["/bin/mkdir",projectPath])

  if checkPresence("%s/.git"%(projectPath)) == 1:
    	logger.debug("No .git folder in %s"%(projectPath))
    	gitClone(gitRepository,projectPath)
  else:
    	logger.debug("Already a clone, making a pull")
    	gitPull(project)

Enfin, il nous reste à exécuter les fonctions les unes après les autres, dans le bon ordre.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  # --- Openning the keychain
  openKeychain(password, keychain)

  # --- Create the target folder in /tmp
  createTargetFolder(targetFolder)

  # --- Increase BuildNumber
  increaseBuildNumber(projectPath,"PillStock","PillStock-Info.plist")

  # --- Compile to .app
  compileApp(SDK, project, configuration, target)

  # --- Transforme the .app into .ipa (in targetFolder)
  createIPA(SDK, workspace, configuration, target, DeveloperName, ProvisionningProfile,targetFolder)

  # --- Extract Info.plsit form .ipa file
  info=retreiveInfo(targetFolder,target)

  # --- Create Manifest.plist
  manifest=createManifest(info,target,targetFolder,deployment_address)

  # --- Create index.hml file
  htmlFile=createIndexHTML(targetFolder,target,manifest)

  # --- Send files on distribution machine
  distribution(remoteHost, username, password, remoteFolder, targetFolder)

Voilà, c’est fini… ou presque.

Il nous reste à voire le fichier de configuration. Celui-ci est très simple et nous n’allons pas exploiter toutes les possibilités de ConfigParser. Nous n’aurons qu’une celle section du nom de [xcodebuild].

Quand au contenu du fichier, c’est encore une fois très simple. Un paramètre, sa valeur 

1
2
3
4
  [xcodebuild]
  SDK=iPhoneos5.
  keychain=jenkins
  password=toto

Attention, le mot de passe est en clair dans ce fichier, a vous de le protéger correctement avec des droits d’accès appropriés (chmod 600 monFichierDeCondfig).

Cette fois-ci, c’est fini, le nouveau script se trouve sur Github, avec un fichier de configuration d’exemple.

N’hésitez pas à intervenir et à enrichir le débat !


Billet Précédent


Laisser un commentaire

Les commentaires sont soumis à modération.