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 'debug-args': directives.unchanged, 48 'debugserver-args': directives.unchanged, 49 'attach-args': directives.unchanged, 50 } 51 52 TOOLS = ['cmake', 'west', 'all'] 53 GENERATORS = ['make', 'ninja'] 54 HOST_OS = ['unix', 'win', 'all'] 55 IN_TREE_STR = '# From the root of the zephyr repository' 56 57 def run(self): 58 # Re-run on the current document if this directive's source changes. 59 self.state.document.settings.env.note_dependency(__file__) 60 61 # Parse directive options. Don't use os.path.sep or os.path.join here! 62 # That would break if building the docs on Windows. 63 tool = self.options.get('tool', 'west').lower() 64 app = self.options.get('app', None) 65 zephyr_app = self.options.get('zephyr-app', None) 66 cd_into = 'cd-into' in self.options 67 generator = self.options.get('generator', 'ninja').lower() 68 host_os = self.options.get('host-os', 'all').lower() 69 board = self.options.get('board', None) 70 shield = self.options.get('shield', None) 71 conf = self.options.get('conf', None) 72 gen_args = self.options.get('gen-args', None) 73 build_args = self.options.get('build-args', None) 74 snippets = self.options.get('snippets', None) 75 build_dir_append = self.options.get('build-dir', '').strip('/') 76 build_dir_fmt = self.options.get('build-dir-fmt', None) 77 goals = self.options.get('goals').split() 78 skip_config = 'maybe-skip-config' in self.options 79 compact = 'compact' in self.options 80 west_args = self.options.get('west-args', None) 81 flash_args = self.options.get('flash-args', None) 82 debug_args = self.options.get('debug-args', None) 83 debugserver_args = self.options.get('debugserver-args', None) 84 attach_args = self.options.get('attach-args', None) 85 86 if tool not in self.TOOLS: 87 raise self.error(f'Unknown tool {tool}; choose from: {self.TOOLS}') 88 89 if app and zephyr_app: 90 raise self.error('Both app and zephyr-app options were given.') 91 92 if build_dir_append != '' and build_dir_fmt: 93 raise self.error('Both build-dir and build-dir-fmt options were given.') 94 95 if build_dir_fmt and tool != 'west': 96 raise self.error('build-dir-fmt is only supported for the west build tool.') 97 98 if generator not in self.GENERATORS: 99 raise self.error(f'Unknown generator {generator}; choose from: {self.GENERATORS}') 100 101 if host_os not in self.HOST_OS: 102 raise self.error(f'Unknown host-os {host_os}; choose from: {self.HOST_OS}') 103 104 if compact and skip_config: 105 raise self.error('Both compact and maybe-skip-config options were given.') 106 107 # as folks might use "<...>" notation to indicate a variable portion of the path, we 108 # deliberately don't check for the validity of such paths. 109 if zephyr_app and not any([x in zephyr_app for x in ["<", ">"]]): 110 app_path = ZEPHYR_BASE / zephyr_app 111 if not app_path.is_dir(): 112 raise self.error( 113 f"zephyr-app: {zephyr_app} is not a valid folder in the zephyr tree." 114 ) 115 116 app = app or zephyr_app 117 in_tree = self.IN_TREE_STR if zephyr_app else None 118 # Allow build directories which are nested. 119 build_dir = ('build' + '/' + build_dir_append).rstrip('/') 120 121 # Prepare repeatable arguments 122 host_os = [host_os] if host_os != "all" else [v for v in self.HOST_OS 123 if v != 'all'] 124 tools = [tool] if tool != "all" else [v for v in self.TOOLS 125 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( 175 f'CMake and {generator}')) 176 content.append(paragraph) 177 content.append(self._lit_block(c)) 178 else: 179 content.extend(c) 180 181 if not tool_comment: 182 content = [self._lit_block(content)] 183 184 return content 185 186 def _lit_block(self, content): 187 content = '\n'.join(content) 188 189 # Create the nodes. 190 literal = nodes.literal_block(content, content) 191 self.add_name(literal) 192 literal['language'] = 'shell' 193 return literal 194 195 def _generate_west(self, **kwargs): 196 content = [] 197 generator = kwargs['generator'] 198 board = kwargs['board'] 199 app = kwargs['app'] 200 in_tree = kwargs['in_tree'] 201 goals = kwargs['goals'] 202 cd_into = kwargs['cd_into'] 203 build_dir = kwargs['build_dir'] 204 build_dir_fmt = kwargs['build_dir_fmt'] 205 compact = kwargs['compact'] 206 shield = kwargs['shield'] 207 snippets = kwargs['snippets'] 208 build_args = kwargs["build_args"] 209 west_args = kwargs['west_args'] 210 flash_args = kwargs['flash_args'] 211 debug_args = kwargs['debug_args'] 212 debugserver_args = kwargs['debugserver_args'] 213 attach_args = kwargs['attach_args'] 214 kwargs['board'] = None 215 # west always defaults to ninja 216 gen_arg = ' -G\'Unix Makefiles\'' if generator == 'make' else '' 217 cmake_args = gen_arg + self._cmake_args(**kwargs) 218 cmake_args = f' --{cmake_args}' if cmake_args != '' else '' 219 build_args = "".join(f" -o {b}" for b in build_args) if build_args else "" 220 west_args = f' {west_args}' if west_args else '' 221 flash_args = f' {flash_args}' if flash_args else '' 222 debug_args = f' {debug_args}' if debug_args else '' 223 debugserver_args = f' {debugserver_args}' if debugserver_args else '' 224 attach_args = f' {attach_args}' if attach_args else '' 225 snippet_args = ''.join(f' -S {s}' for s in snippets) if snippets else '' 226 shield_args = ''.join(f' --shield {s}' for s in shield) if shield else '' 227 # ignore zephyr_app since west needs to run within 228 # the installation. Instead rely on relative path. 229 src = f' {app}' if app and not cd_into else '' 230 231 if build_dir_fmt is None: 232 dst = f' -d {build_dir}' if build_dir != 'build' else '' 233 build_dst = dst 234 else: 235 app_name = app.split('/')[-1] 236 build_dir_formatted = build_dir_fmt.format(app=app_name, board=board, source_dir=app) 237 dst = f' -d {build_dir_formatted}' 238 build_dst = '' 239 240 if in_tree and not compact: 241 content.append(in_tree) 242 243 if cd_into and app: 244 content.append(f'cd {app}') 245 246 # We always have to run west build. 247 # 248 # FIXME: doing this unconditionally essentially ignores the 249 # maybe-skip-config option if set. 250 # 251 # This whole script and its users from within the 252 # documentation needs to be overhauled now that we're 253 # defaulting to west. 254 # 255 # For now, this keeps the resulting commands working. 256 content.append( 257 f"west build -b {board}{build_args}{west_args}{snippet_args}" 258 f"{shield_args}{build_dst}{src}{cmake_args}" 259 ) 260 261 # If we're signing, we want to do that next, so that flashing 262 # etc. commands can use the signed file which must be created 263 # in this step. 264 if 'sign' in goals: 265 content.append(f'west sign{dst}') 266 267 for goal in goals: 268 if goal in {'build', 'sign'}: 269 continue 270 elif goal == 'flash': 271 content.append(f'west flash{flash_args}{dst}') 272 elif goal == 'debug': 273 content.append(f'west debug{debug_args}{dst}') 274 elif goal == 'debugserver': 275 content.append(f'west debugserver{debugserver_args}{dst}') 276 elif goal == 'attach': 277 content.append(f'west attach{attach_args}{dst}') 278 else: 279 content.append(f'west build -t {goal}{dst}') 280 281 return content 282 283 @staticmethod 284 def _mkdir(mkdir, build_dir, host_os, skip_config): 285 content = [] 286 if skip_config: 287 content.append(f"# If you already made a build directory ({build_dir}) and ran cmake, " 288 f"just 'cd {build_dir}' instead.") 289 if host_os == 'all': 290 content.append(f'mkdir {build_dir} && cd {build_dir}') 291 if host_os == "unix": 292 content.append(f'{mkdir} {build_dir} && cd {build_dir}') 293 elif host_os == "win": 294 build_dir = build_dir.replace('/', '\\') 295 content.append(f'mkdir {build_dir} & cd {build_dir}') 296 return content 297 298 @staticmethod 299 def _cmake_args(**kwargs): 300 board = kwargs['board'] 301 conf = kwargs['conf'] 302 gen_args = kwargs['gen_args'] 303 board_arg = f' -DBOARD={board}' if board else '' 304 conf_arg = f' -DCONF_FILE={conf}' if conf else '' 305 gen_args = f' {gen_args}' if gen_args else '' 306 307 return f'{board_arg}{conf_arg}{gen_args}' 308 309 def _cd_into(self, mkdir, **kwargs): 310 app = kwargs['app'] 311 host_os = kwargs['host_os'] 312 compact = kwargs['compact'] 313 build_dir = kwargs['build_dir'] 314 skip_config = kwargs['skip_config'] 315 content = [] 316 os_comment = None 317 if len(host_os) > 1: 318 os_comment = '# On {}' 319 num_slashes = build_dir.count('/') 320 if not app and mkdir and num_slashes == 0: 321 # When there's no app and a single level deep build dir, 322 # simplify output 323 content.extend(self._mkdir(mkdir, build_dir, 'all', 324 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(f'# If you already ran cmake with -B{build_dir}, you ' 387 f'can skip this step and run {generator} directly.') 388 else: 389 content.append(f'# Use cmake to configure a {generator.capitalize()}-based build' 390 'system:') 391 392 content.append(f'cmake{cmake_build_dir}{gen_arg}{cmake_args}{snippet_args}{shield_args}{source_dir}') 393 if not compact: 394 content.extend(['', 395 '# Now run the build tool on the generated build system:']) 396 397 if 'build' in goals: 398 content.append(f'{generator}{tool_build_dir}{build_args}') 399 for goal in goals: 400 if goal == 'build': 401 continue 402 content.append(f'{generator}{tool_build_dir} {goal}') 403 404 return content 405 406 407def setup(app): 408 app.add_directive('zephyr-app-commands', ZephyrAppCommandsDirective) 409 410 return { 411 'version': '1.0', 412 'parallel_read_safe': True, 413 'parallel_write_safe': True 414 } 415