1# Copyright (c) 2017 Open Source Foundries Limited. 2# 3# SPDX-License-Identifier: Apache-2.0 4 5'''Sphinx extensions related to managing Zephyr applications.''' 6 7from pathlib import Path 8 9from docutils import nodes 10from docutils.parsers.rst import Directive, directives 11 12ZEPHYR_BASE = Path(__file__).parents[3] 13 14# TODO: extend and modify this for Windows. 15# 16# This could be as simple as generating a couple of sets of instructions, one 17# for Unix environments, and another for Windows. 18class ZephyrAppCommandsDirective(Directive): 19 r''' 20 This is a Zephyr directive for generating consistent documentation 21 of the shell commands needed to manage (build, flash, etc.) an application. 22 ''' 23 has_content = False 24 required_arguments = 0 25 optional_arguments = 0 26 final_argument_whitespace = False 27 option_spec = { 28 'tool': directives.unchanged, 29 'app': directives.unchanged, 30 'zephyr-app': directives.unchanged, 31 'cd-into': directives.flag, 32 'generator': directives.unchanged, 33 'host-os': directives.unchanged, 34 'board': directives.unchanged, 35 'shield': directives.unchanged, 36 'conf': directives.unchanged, 37 'gen-args': directives.unchanged, 38 'build-args': directives.unchanged, 39 'snippets': directives.unchanged, 40 'build-dir': directives.unchanged, 41 'build-dir-fmt': directives.unchanged, 42 'goals': directives.unchanged_required, 43 'maybe-skip-config': directives.flag, 44 'compact': directives.flag, 45 'west-args': directives.unchanged, 46 'flash-args': directives.unchanged, 47 } 48 49 TOOLS = ['cmake', 'west', 'all'] 50 GENERATORS = ['make', 'ninja'] 51 HOST_OS = ['unix', 'win', 'all'] 52 IN_TREE_STR = '# From the root of the zephyr repository' 53 54 def run(self): 55 # Re-run on the current document if this directive's source changes. 56 self.state.document.settings.env.note_dependency(__file__) 57 58 # Parse directive options. Don't use os.path.sep or os.path.join here! 59 # That would break if building the docs on Windows. 60 tool = self.options.get('tool', 'west').lower() 61 app = self.options.get('app', None) 62 zephyr_app = self.options.get('zephyr-app', None) 63 cd_into = 'cd-into' in self.options 64 generator = self.options.get('generator', 'ninja').lower() 65 host_os = self.options.get('host-os', 'all').lower() 66 board = self.options.get('board', None) 67 shield = self.options.get('shield', None) 68 conf = self.options.get('conf', None) 69 gen_args = self.options.get('gen-args', None) 70 build_args = self.options.get('build-args', None) 71 snippets = self.options.get('snippets', None) 72 build_dir_append = self.options.get('build-dir', '').strip('/') 73 build_dir_fmt = self.options.get('build-dir-fmt', None) 74 goals = self.options.get('goals').split() 75 skip_config = 'maybe-skip-config' in self.options 76 compact = 'compact' in self.options 77 west_args = self.options.get('west-args', None) 78 flash_args = self.options.get('flash-args', None) 79 80 if tool not in self.TOOLS: 81 raise self.error(f'Unknown tool {tool}; choose from: {self.TOOLS}') 82 83 if app and zephyr_app: 84 raise self.error('Both app and zephyr-app options were given.') 85 86 if build_dir_append != '' and build_dir_fmt: 87 raise self.error('Both build-dir and build-dir-fmt options were given.') 88 89 if build_dir_fmt and tool != 'west': 90 raise self.error('build-dir-fmt is only supported for the west build tool.') 91 92 if generator not in self.GENERATORS: 93 raise self.error(f'Unknown generator {generator}; choose from: {self.GENERATORS}') 94 95 if host_os not in self.HOST_OS: 96 raise self.error(f'Unknown host-os {host_os}; choose from: {self.HOST_OS}') 97 98 if compact and skip_config: 99 raise self.error('Both compact and maybe-skip-config options were given.') 100 101 # as folks might use "<...>" notation to indicate a variable portion of the path, we 102 # deliberately don't check for the validity of such paths. 103 if zephyr_app and not any([x in zephyr_app for x in ["<", ">"]]): 104 app_path = ZEPHYR_BASE / zephyr_app 105 if not app_path.is_dir(): 106 raise self.error( 107 f"zephyr-app: {zephyr_app} is not a valid folder in the zephyr tree." 108 ) 109 110 app = app or zephyr_app 111 in_tree = self.IN_TREE_STR if zephyr_app else None 112 # Allow build directories which are nested. 113 build_dir = ('build' + '/' + build_dir_append).rstrip('/') 114 115 # Prepare repeatable arguments 116 host_os = [host_os] if host_os != "all" else [v for v in self.HOST_OS 117 if v != 'all'] 118 tools = [tool] if tool != "all" else [v for v in self.TOOLS 119 if v != 'all'] 120 build_args_list = build_args.split(' ') if build_args is not None else None 121 snippet_list = snippets.split(',') if snippets is not None else None 122 shield_list = shield.split(',') if shield is not None else None 123 124 # Build the command content as a list, then convert to string. 125 content = [] 126 tool_comment = None 127 if len(tools) > 1: 128 tool_comment = 'Using {}:' 129 130 run_config = { 131 'host_os': host_os, 132 'app': app, 133 'in_tree': in_tree, 134 'cd_into': cd_into, 135 'board': board, 136 'shield': shield_list, 137 'conf': conf, 138 'gen_args': gen_args, 139 'build_args': build_args_list, 140 'snippets': snippet_list, 141 'build_dir': build_dir, 142 'build_dir_fmt': build_dir_fmt, 143 'goals': goals, 144 'compact': compact, 145 'skip_config': skip_config, 146 'generator': generator, 147 'west_args': west_args, 148 'flash_args': flash_args, 149 } 150 151 if 'west' in tools: 152 w = self._generate_west(**run_config) 153 if tool_comment: 154 paragraph = nodes.paragraph() 155 paragraph += nodes.Text(tool_comment.format('west')) 156 content.append(paragraph) 157 content.append(self._lit_block(w)) 158 else: 159 content.extend(w) 160 161 if 'cmake' in tools: 162 c = self._generate_cmake(**run_config) 163 if tool_comment: 164 paragraph = nodes.paragraph() 165 paragraph += nodes.Text(tool_comment.format( 166 f'CMake and {generator}')) 167 content.append(paragraph) 168 content.append(self._lit_block(c)) 169 else: 170 content.extend(c) 171 172 if not tool_comment: 173 content = [self._lit_block(content)] 174 175 return content 176 177 def _lit_block(self, content): 178 content = '\n'.join(content) 179 180 # Create the nodes. 181 literal = nodes.literal_block(content, content) 182 self.add_name(literal) 183 literal['language'] = 'shell' 184 return literal 185 186 def _generate_west(self, **kwargs): 187 content = [] 188 generator = kwargs['generator'] 189 board = kwargs['board'] 190 app = kwargs['app'] 191 in_tree = kwargs['in_tree'] 192 goals = kwargs['goals'] 193 cd_into = kwargs['cd_into'] 194 build_dir = kwargs['build_dir'] 195 build_dir_fmt = kwargs['build_dir_fmt'] 196 compact = kwargs['compact'] 197 shield = kwargs['shield'] 198 snippets = kwargs['snippets'] 199 build_args = kwargs["build_args"] 200 west_args = kwargs['west_args'] 201 flash_args = kwargs['flash_args'] 202 kwargs['board'] = None 203 # west always defaults to ninja 204 gen_arg = ' -G\'Unix Makefiles\'' if generator == 'make' else '' 205 cmake_args = gen_arg + self._cmake_args(**kwargs) 206 cmake_args = f' --{cmake_args}' if cmake_args != '' else '' 207 build_args = "".join(f" -o {b}" for b in build_args) if build_args else "" 208 west_args = f' {west_args}' if west_args else '' 209 flash_args = f' {flash_args}' if flash_args else '' 210 snippet_args = ''.join(f' -S {s}' for s in snippets) if snippets else '' 211 shield_args = ''.join(f' --shield {s}' for s in shield) if shield else '' 212 # ignore zephyr_app since west needs to run within 213 # the installation. Instead rely on relative path. 214 src = f' {app}' if app and not cd_into else '' 215 216 if build_dir_fmt is None: 217 dst = f' -d {build_dir}' if build_dir != 'build' else '' 218 build_dst = dst 219 else: 220 app_name = app.split('/')[-1] 221 build_dir_formatted = build_dir_fmt.format(app=app_name, board=board, source_dir=app) 222 dst = f' -d {build_dir_formatted}' 223 build_dst = '' 224 225 if in_tree and not compact: 226 content.append(in_tree) 227 228 if cd_into and app: 229 content.append(f'cd {app}') 230 231 # We always have to run west build. 232 # 233 # FIXME: doing this unconditionally essentially ignores the 234 # maybe-skip-config option if set. 235 # 236 # This whole script and its users from within the 237 # documentation needs to be overhauled now that we're 238 # defaulting to west. 239 # 240 # For now, this keeps the resulting commands working. 241 content.append( 242 f"west build -b {board}{build_args}{west_args}{snippet_args}" 243 f"{shield_args}{build_dst}{src}{cmake_args}" 244 ) 245 246 # If we're signing, we want to do that next, so that flashing 247 # etc. commands can use the signed file which must be created 248 # in this step. 249 if 'sign' in goals: 250 content.append(f'west sign{dst}') 251 252 for goal in goals: 253 if goal in {'build', 'sign'}: 254 continue 255 elif goal == 'flash': 256 content.append(f'west flash{flash_args}{dst}') 257 elif goal == 'debug': 258 content.append(f'west debug{dst}') 259 elif goal == 'debugserver': 260 content.append(f'west debugserver{dst}') 261 elif goal == 'attach': 262 content.append(f'west attach{dst}') 263 else: 264 content.append(f'west build -t {goal}{dst}') 265 266 return content 267 268 @staticmethod 269 def _mkdir(mkdir, build_dir, host_os, skip_config): 270 content = [] 271 if skip_config: 272 content.append(f"# If you already made a build directory ({build_dir}) and ran cmake, " 273 f"just 'cd {build_dir}' instead.") 274 if host_os == 'all': 275 content.append(f'mkdir {build_dir} && cd {build_dir}') 276 if host_os == "unix": 277 content.append(f'{mkdir} {build_dir} && cd {build_dir}') 278 elif host_os == "win": 279 build_dir = build_dir.replace('/', '\\') 280 content.append(f'mkdir {build_dir} & cd {build_dir}') 281 return content 282 283 @staticmethod 284 def _cmake_args(**kwargs): 285 board = kwargs['board'] 286 conf = kwargs['conf'] 287 gen_args = kwargs['gen_args'] 288 board_arg = f' -DBOARD={board}' if board else '' 289 conf_arg = f' -DCONF_FILE={conf}' if conf else '' 290 gen_args = f' {gen_args}' if gen_args else '' 291 292 return f'{board_arg}{conf_arg}{gen_args}' 293 294 def _cd_into(self, mkdir, **kwargs): 295 app = kwargs['app'] 296 host_os = kwargs['host_os'] 297 compact = kwargs['compact'] 298 build_dir = kwargs['build_dir'] 299 skip_config = kwargs['skip_config'] 300 content = [] 301 os_comment = None 302 if len(host_os) > 1: 303 os_comment = '# On {}' 304 num_slashes = build_dir.count('/') 305 if not app and mkdir and num_slashes == 0: 306 # When there's no app and a single level deep build dir, 307 # simplify output 308 content.extend(self._mkdir(mkdir, build_dir, 'all', 309 skip_config)) 310 if not compact: 311 content.append('') 312 return content 313 for host in host_os: 314 if host == "unix": 315 if os_comment: 316 content.append(os_comment.format('Linux/macOS')) 317 if app: 318 content.append(f'cd {app}') 319 elif host == "win": 320 if os_comment: 321 content.append(os_comment.format('Windows')) 322 if app: 323 backslashified = app.replace('/', '\\') 324 content.append(f'cd {backslashified}') 325 if mkdir: 326 content.extend(self._mkdir(mkdir, build_dir, host, skip_config)) 327 if not compact: 328 content.append('') 329 return content 330 331 def _generate_cmake(self, **kwargs): 332 generator = kwargs['generator'] 333 cd_into = kwargs['cd_into'] 334 app = kwargs['app'] 335 in_tree = kwargs['in_tree'] 336 build_dir = kwargs['build_dir'] 337 build_args = kwargs['build_args'] 338 snippets = kwargs['snippets'] 339 shield = kwargs['shield'] 340 skip_config = kwargs['skip_config'] 341 goals = kwargs['goals'] 342 compact = kwargs['compact'] 343 344 content = [] 345 346 if in_tree and not compact: 347 content.append(in_tree) 348 349 if cd_into: 350 num_slashes = build_dir.count('/') 351 mkdir = 'mkdir' if num_slashes == 0 else 'mkdir -p' 352 content.extend(self._cd_into(mkdir, **kwargs)) 353 # Prepare cmake/ninja/make variables 354 source_dir = ' ' + '/'.join(['..' for i in range(num_slashes + 1)]) 355 cmake_build_dir = '' 356 tool_build_dir = '' 357 else: 358 source_dir = f' {app}' if app else ' .' 359 cmake_build_dir = f' -B{build_dir}' 360 tool_build_dir = f' -C{build_dir}' 361 362 # Now generate the actual cmake and make/ninja commands 363 gen_arg = ' -GNinja' if generator == 'ninja' else '' 364 build_args = f' {build_args}' if build_args else '' 365 snippet_args = ' -DSNIPPET="{}"'.format(';'.join(snippets)) if snippets else '' 366 shield_args = ' -DSHIELD="{}"'.format(';'.join(shield)) if shield else '' 367 cmake_args = self._cmake_args(**kwargs) 368 369 if not compact: 370 if not cd_into and skip_config: 371 content.append(f'# If you already ran cmake with -B{build_dir}, you ' 372 f'can skip this step and run {generator} directly.') 373 else: 374 content.append(f'# Use cmake to configure a {generator.capitalize()}-based build' 375 'system:') 376 377 content.append(f'cmake{cmake_build_dir}{gen_arg}{cmake_args}{snippet_args}{shield_args}{source_dir}') 378 if not compact: 379 content.extend(['', 380 '# Now run the build tool on the generated build system:']) 381 382 if 'build' in goals: 383 content.append(f'{generator}{tool_build_dir}{build_args}') 384 for goal in goals: 385 if goal == 'build': 386 continue 387 content.append(f'{generator}{tool_build_dir} {goal}') 388 389 return content 390 391 392def setup(app): 393 app.add_directive('zephyr-app-commands', ZephyrAppCommandsDirective) 394 395 return { 396 'version': '1.0', 397 'parallel_read_safe': True, 398 'parallel_write_safe': True 399 } 400