2 # Copyright (c) 2000 - 2016 Samsung Electronics Co., Ltd. All rights reserved.
5 # @author Chulwoo Shin <cw1.shin@samsung.com>
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
29 from datetime import datetime
30 from tic.utils.error import TICError
31 from tic.utils.file import write, make_dirs
32 from tic.config import configmgr
34 DUMMY_PLATFORM = 'DummyPlatform'
35 DEFAULT_RECIPE_NAME = 'default_recipe'
36 DEFAULT_RECIPE_PATH = configmgr.setting['default_recipe']
37 RECIPE_EXTEND_FIELD = {'Repos', 'Groups', 'Repositories', 'Partitions', 'ExtraPackages', 'RemovePackages', 'PostScripts', 'NoChrootScripts'}
39 class DefaultRecipe(object):
40 DEFAULT_RECIPE = {'NoChrootScripts': [{'Contents': 'if [ -n "$IMG_NAME" ]; then\n echo "BUILD_ID=$IMG_NAME" >> $INSTALL_ROOT/etc/tizen-release\n echo "BUILD_ID=$IMG_NAME" >> $INSTALL_ROOT/etc/os-release\nfi\n',
41 'Name': 'buildname'}],
42 'Partitions': [{'Contents': 'part / --size=2000 --ondisk mmcblk0p --fstype=ext4 --label=rootfs --extoptions="-J size=16"\npart /opt/ --size=1000 --ondisk mmcblk0p --fstype=ext4 --label=system-data --extoptions="-m 0"\npart /boot/kernel/mod_tizen_tm1/lib/modules --size=12 --ondisk mmcblk0p --fstype=ext4 --label=modules\n',
43 'Name': 'default-part'}],
44 'PostScripts': [{'Contents': '#!/bin/sh\necho "#################### generic-base.post ####################"\n\ntest ! -e /opt/var && mkdir -p /opt/var\ntest -d /var && cp -arf /var/* /opt/var/\nrm -rf /var\nln -snf opt/var /var\n\ntest ! -e /opt/usr/home && mkdir -p /opt/usr/home\ntest -d /home && cp -arf /home/* /opt/usr/home/\nrm -rf /home\nln -snf opt/usr/home /home\n\nbuild_ts=$(date -u +%s)\nbuild_date=$(date -u --date @$build_ts +%Y%m%d_%H%M%S)\nbuild_time=$(date -u --date @$build_ts +%H:%M:%S)\n\nsed -ri \\\n\t-e \'s|@BUILD_ID[@]|@BUILD_ID@|g\' \\\n\t-e "s|@BUILD_DATE[@]|$build_date|g" \\\n\t-e "s|@BUILD_TIME[@]|$build_time|g" \\\n\t-e "s|@BUILD_TS[@]|$build_ts|g" \\\n\t/etc/tizen-build.conf\n\n# setup systemd default target for user session\ncat <<\'EOF\' >>/usr/lib/systemd/user/default.target\n[Unit]\nDescription=User session default target\nEOF\nmkdir -p /usr/lib/systemd/user/default.target.wants\n\n# sdx: fix smack labels on /var/log\nchsmack -a \'*\' /var/log\n\n# create appfw dirs inside homes\nfunction generic_base_user_exists() {\n user=$1\n getent passwd | grep -q ^${user}:\n}\n\nfunction generic_base_user_home() {\n user=$1\n getent passwd | grep ^${user}: | cut -f6 -d\':\'\n}\n\nfunction generic_base_fix_user_homedir() {\n user=$1\n generic_base_user_exists $user || return 1\n\nhomedir=$(generic_base_user_home $user)\n mkdir -p $homedir/apps_rw\n for appdir in desktop manifest dbspace; do\n mkdir -p $homedir/.applications/$appdir\n done\n find $homedir -type d -exec chsmack -a User {} \\;\n chown -R $user:users $homedir\n return 0\n}\n\n# fix TC-320 for SDK\n. /etc/tizen-build.conf\n[ "${TZ_BUILD_WITH_EMULATOR}" == "1" ] && generic_base_fix_user_homedir developer\n\n# Add info.ini for system-info CAPI (TC-2047)\n/etc/make_info_file.sh',
45 'Name': 'generic-base'}],
46 'Recipe': {'Active': True,
47 'Architecture': 'armv7l',
50 'BootloaderAppend': 'rw vga=current splash rootwait rootfstype=ext4 plymouth.enable=0',
51 'BootloaderOptions': '--ptable=gpt --menus="install:Wipe and Install:systemd.unit=system-installer.service:test"',
52 'BootloaderTimeout': 3,
53 'DefaultUser': 'guest',
54 'DefaultUserPass': 'tizen',
57 'FileName': 'default-armv7l',
60 'Language': 'en_US.UTF-8',
61 'Mic2Options': '-f raw --fstab=uuid --copy-kernel --compress-disk-image=bz2 --generate-bmap',
62 'Name': 'default-recipe',
63 'NoChrootScripts': ['buildname'],
64 'Part': 'default-part',
65 'PostScripts': ['generic-base'],
67 'Repos': ['tizen_unified', 'tizen_base_armv7l'],
72 'Timezone': 'Asia/Seoul',
73 'UserGroups': 'audio,video'},
74 'Repositories': [{'Name': 'tizen_unified',
75 'Options': '--ssl_verify=no',
76 'Url': 'http://download.tizen.org/snapshots/tizen/unified/latest/repos/standard/packages/'},
77 {'Name': 'tizen_base_armv7l',
78 'Options': '--ssl_verify=no',
79 'Url': 'http://download.tizen.org/snapshots/tizen/base/latest/repos/arm/packages/'}]}
81 def __new__(cls, *args, **kwargs):
83 cls._instance = super(DefaultRecipe, cls).__new__(cls, *args, **kwargs)
86 logger = logging.getLogger(__name__)
87 if os.path.exists(DEFAULT_RECIPE_PATH):
89 with file(DEFAULT_RECIPE_PATH) as f:
90 self.DEFAULT_RECIPE = yaml.load(f)
91 logger.info('Read default recipe from %s' % DEFAULT_RECIPE_PATH)
92 except IOError as err:
94 except yaml.YAMLError as err:
96 def getDefaultRecipe(self):
97 return copy.deepcopy(self.DEFAULT_RECIPE)
98 def getSystemConfig(self):
99 data = copy.deepcopy(self.DEFAULT_RECIPE)
100 for field in RECIPE_EXTEND_FIELD:
101 if field == 'Partitions':
103 if data['Recipe'].get(field):
104 data['Recipe'][field] = []
108 def getDefaultParameter(self):
109 return [dict(url=DEFAULT_RECIPE_NAME, type='recipe')]
111 default_recipe = DefaultRecipe()
113 class RecipeParser(object):
114 def __init__(self, inputs):
115 # in order to priority definition
118 self._repositories = None
121 self.addRecipes(inputs)
124 logger = logging.getLogger(__name__)
127 self._repositories = None
131 for data in self.inputs:
132 data_type = data.get('type')
133 # type: recipe or repository
134 if data_type == 'recipe':
136 if data.get('url') == DEFAULT_RECIPE_NAME:
137 self.recipes[data.get('url')] = default_recipe.getDefaultRecipe()
139 with contextlib.closing(urllib2.urlopen(data.get('url'))) as op:
140 self.recipes[data.get('url')] = yaml.load(op.read())
141 elif data_type == 'repository':
142 data['name'] = 'repository_%s' % repo_count
144 except urllib2.HTTPError as err:
146 msg = configmgr.message['recipe_not_found'] % data.get('url')
151 except urllib2.URLError as err:
153 raise TICError(configmgr.message['server_error'])
154 except yaml.YAMLError as err:
156 raise TICError(configmgr.message['recipe_parse_error'] % data.get('url'))
158 def addRecipes(self, inputs):
160 if isinstance(inputs, list):
162 self.inputs.append(data)
164 self.inputs.append(inputs)
166 def getRepositories(self):
167 if not self._repositories:
168 self._repositories = self._getAllRepositories()
169 return self._repositories
171 def _getAllRepositories(self):
174 for data in self.inputs:
175 if data.get('type') == 'recipe':
177 recipe_info = self.recipes[data['url']]
179 if recipe_info.get('Recipe'):
180 if recipe_info['Recipe'].get('Name'):
181 recipe_name = recipe_info['Recipe'].get('Name')
182 if recipe_info['Recipe'].get('Repos'):
183 for repo_name in recipe_info['Recipe'].get('Repos'):
185 if recipe_info.get('Repositories'):
186 for repo_info in recipe_info.get('Repositories'):
187 if repo_info.get('Name') == repo_name:
188 recipe_repos.append(dict(name=repo_name,
189 url=repo_info.get('Url'),
190 options=repo_info.get('Options')))
193 # repository does not exist
195 raise TICError(configmgr.message['recipe_repo_not_exist'] % repo_name)
197 recipe_name = 'recipe_%s' % name_count
199 repos.append(dict(name=recipe_name,
207 def _renameRepository(self, repo_dict, repo_name):
208 number = repo_dict.get(repo_name)
209 new_name = ''.join([repo_name, '_', str(number)])
210 while(new_name in repo_dict):
212 new_name = ''.join([repo_name, '_', str(number)])
213 repo_dict[repo_name] = number + 1
216 def getMergedRepositories(self):
218 repositories = self.getRepositories()
219 repo_name = {} # 'name': count
220 repo_url = {} # 'url': exist
221 for target in repositories:
222 if target.get('type') == 'recipe':
223 if target.get('repos'):
224 for repo in target.get('repos'):
225 # if repo's url is duplicated, remove it.
226 if repo.get('url') in repo_url:
228 # if repo's name is duplicated, rename it (postfix '_count')
229 if repo.get('name') in repo_name:
230 repo['name'] = self._renameRepository(repo_name, repo['name'])
232 repo_name[repo['name']] = 1
233 repo_url[repo['url']] = 1
236 # recipe does not have repository information
238 elif(target.get('type') == 'repository'):
239 # if repo's url is duplicated, remove it.
240 if target.get('url') in repo_url:
242 if target['name'] in repo_name:
243 target['name'] = self._renameRepository(repo_name, target['name'])
245 repo_name[target['name']] = 1
246 repo_url[target['url']] = 1
247 result.append(target)
250 def getMergedRecipe(self):
254 mergedInfo = default_recipe.getSystemConfig()
256 for i in xrange(len(self.inputs), 0, -1):
257 if self.inputs[i-1].get('type') == 'recipe':
258 recipe = self.recipes[self.inputs[i-1].get('url')]
259 if recipe.get('Recipe'):
260 for k, v in recipe.get('Recipe').iteritems():
263 if k in RECIPE_EXTEND_FIELD:
266 for j in xrange(len(v), 0, -1):
267 mergedInfo['Recipe'][k].append(v[j-1])
269 mergedInfo['Recipe'][k] = v
270 for fieldName in RECIPE_EXTEND_FIELD:
271 if recipe.get(fieldName):
272 if fieldName == 'Repositories':
274 for data in recipe.get(fieldName):
275 mergedInfo[fieldName].append(data)
277 for extName in RECIPE_EXTEND_FIELD:
278 if mergedInfo['Recipe'].get(extName):
279 mergedInfo['Recipe'][extName].reverse()
280 if mergedInfo.get(extName):
281 mergedInfo[extName].reverse()
284 mergedInfo['Repositories'] = self.getMergedRepositories()
285 if mergedInfo.get('Repositories'):
286 for repo in mergedInfo['Repositories']:
287 mergedInfo['Recipe']['Repos'].append(repo['name'])
290 def export2Recipe(self, packages, outdir, filename='recipe.yaml'):
291 logger = logging.getLogger(__name__)
292 recipe = self.getMergedRecipe()
294 reciep_path = os.path.join(outdir, filename)
297 recipe['Recipe']['ExtraPackages'] = packages
299 if 'Repositories' in recipe:
301 for repo in recipe.get('Repositories'):
302 repos.append(dict(Name= repo.get('name'),
303 Url= repo.get('url'),
304 Options = repo.get('options')))
305 recipe['Repositories'] = repos
308 with open(reciep_path, 'w') as outfile:
309 yaml.safe_dump(recipe, outfile, line_break="\n", width=1000, default_flow_style=False)
310 #outfile.write(stream.replace('\n', '\n\n'))
311 if not os.path.exists(reciep_path):
312 raise TICError('No recipe file was created')
313 except IOError as err:
315 raise TICError('Could not read the recipe files')
316 except yaml.YAMLError as err:
318 raise TICError(configmgr.message['recipe_convert_error'])
321 def export2Yaml(self, packages, filepath):
322 logger = logging.getLogger(__name__)
323 recipe = self.getMergedRecipe()
325 config = dict(Default=None, Configurations=[])
326 config['Default'] = recipe.get('Recipe')
328 config['Default']['ExtraPackages'] = packages
329 # targets (only one target)
330 extraconfs = dict(Platform=DUMMY_PLATFORM,
332 Name= recipe['Recipe'].get('Name'),
333 FileName= recipe['Recipe'].get('FileName'),
334 Part= recipe['Recipe'].get('Part'))
335 config['Configurations'].append(extraconfs)
336 config[DUMMY_PLATFORM] = dict(ExtraPackages=[])
338 dir_path = os.path.join(filepath, datetime.now().strftime('%Y%m%d%H%M%S%f'))
340 logger.info('kickstart cache dir=%s' % dir_path)
342 yamlinfo = YamlInfo(dir_path,
343 os.path.join(dir_path, 'configs.yaml'),
344 os.path.join(dir_path, 'repos.yaml'))
347 with open(yamlinfo.configs, 'w') as outfile:
348 yaml.safe_dump(config, outfile, default_flow_style=False)
351 if 'Repositories' in recipe:
352 repos = dict(Repositories= [])
353 for repo in recipe.get('Repositories'):
354 repos['Repositories'].append(dict(Name= repo.get('name'),
355 Url= repo.get('url'),
356 Options = repo.get('options')))
357 with open(yamlinfo.repos, 'w') as outfile:
358 yaml.safe_dump(repos, outfile, default_flow_style=False)
361 if 'Partitions' in recipe:
362 for partition in recipe.get('Partitions'):
363 partition_path = os.path.join(dir_path, 'partitions')
364 file_name = partition.get('Name')
365 temp = os.path.join(partition_path, file_name)
366 write(temp, partition['Contents'])
369 if 'PostScripts' in recipe:
370 for script in recipe.get('PostScripts'):
371 script_path = os.path.join(dir_path, 'scripts')
372 file_name = '%s.post' % script.get('Name')
373 write(os.path.join(script_path, file_name), script['Contents'])
374 if 'NoChrootScripts' in recipe:
375 for script in recipe.get('NoChrootScripts'):
376 script_path = os.path.join(dir_path, 'scripts')
377 file_name = '%s.nochroot' % script.get('Name')
378 write(os.path.join(script_path, file_name), script['Contents'])
382 logger = logging.getLogger(__name__)
384 with file(path) as f:
386 except IOError as err:
388 raise TICError(configmgr.message['server_error'])
389 except yaml.YAMLError as err:
391 raise TICError(configmgr.message['recipe_parse_error'] % os.path.basename(path))
393 YamlType = collections.namedtuple('YamlInfo', 'cachedir, configs, repos')
394 def YamlInfo(cachedir, configs, repos):
395 return YamlType(cachedir, configs, repos)
397 if __name__ == '__main__':
398 inputs = [{'url': DEFAULT_RECIPE_NAME, 'type': 'recipe'}, {'url': 'http://localhost/repo/recipe/recipe1.yaml', 'type': 'recipe'}]
399 parser = RecipeParser()
400 parser.addRecipes(inputs)
402 print(parser.repositories)