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