├── .github ├── ado │ └── azure-pipeline.yml └── workflows │ └── run-tests.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── action.yml ├── dist ├── index.js ├── licenses.txt └── vc_env.bat ├── index.js ├── package-lock.json ├── package.json └── test ├── cmake-api-tests.js └── sample ├── CMakeLists.txt ├── Custom.ruleset ├── Invalid.ruleset ├── failure ├── CMakeLists.txt └── fail.cpp ├── include ├── regular │ └── regular.h └── system │ └── system.h └── src ├── fileA.cpp └── fileB.cpp /.github/ado/azure-pipeline.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pool: 5 | vmImage: ubuntu-latest 6 | 7 | variables: 8 | - name: Codeql.Enabled 9 | value: true 10 | - name: Codeql.TSAEnabled 11 | value: true 12 | 13 | steps: 14 | - script: echo CodeQL plugin will be auto injected on default branch 15 | displayName: 'Running pipeline for static analysis' 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: 'run-tests' 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 2 * * SUN' 10 | 11 | env: 12 | sample: '${{ github.workspace }}/test/sample' 13 | build: '${{ github.workspace }}/test/sample/build' 14 | result: '${{ github.workspace }}/test/sample/build/results.sarif' 15 | config: Release 16 | 17 | jobs: 18 | build_and_test: 19 | name: Build and Test 20 | runs-on: windows-latest 21 | 22 | steps: 23 | - name: Checkout action 24 | uses: actions/checkout@v2.3.4 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v2.4.0 28 | with: 29 | node-version: '12.x' 30 | - name: Build 31 | run: npm install 32 | - name: Test 33 | run: npm test 34 | 35 | functional_test: 36 | name: Functional - ${{ matrix.test }} 37 | runs-on: windows-latest 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | include: 42 | - test: Regular 43 | outcome: success 44 | ruleset: NativeRecommendedRules.ruleset 45 | ignoredPaths: test/sample/failure 46 | - test: Custom ruleset 47 | outcome: success 48 | ruleset: test/sample/Custom.ruleset 49 | ignoredPaths: test/sample/failure 50 | - test: Invalid ruleset 51 | outcome: failure 52 | ruleset: test/sample/Invalid.ruleset 53 | ignoredPaths: test/sample/failure 54 | - test: Compilation Error 55 | outcome: failure 56 | ruleset: NativeRecommendedRules.ruleset 57 | ignoredPaths: '' 58 | 59 | steps: 60 | - name: Checkout action 61 | uses: actions/checkout@v2.3.4 62 | 63 | - name: Configure CMake 64 | run: cmake ${{ env.sample }}/CMakeLists.txt -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} 65 | 66 | - name: Run action 67 | id: run_action 68 | continue-on-error: true 69 | uses: ./ 70 | with: 71 | cmakeBuildDirectory: ${{ env.build }} 72 | buildConfiguration: ${{ env.config }} 73 | resultsPath: ${{ env.result }} 74 | ruleset: ${{ matrix.ruleset }} 75 | ignoreSystemHeaders: true 76 | ignoredPaths: ${{ matrix.ignoredPaths }} 77 | 78 | - name: Validate expected action outcome 79 | if: steps.run_action.outcome != matrix.outcome 80 | run: exit 1 81 | 82 | - name: Upload SARIF artifact 83 | if: steps.run_action.outcome == 'success' 84 | uses: actions/upload-artifact@v2 85 | with: 86 | name: sarif-file 87 | path: ${{ steps.run_action.outputs.sarif }} 88 | if-no-files-found: error -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules/ 3 | test/sample/build/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft C++ Code Analysis Action 2 | 3 | This actions run code analysis for any CMake project built with the Microsoft Visual C++ Compiler. The analysis 4 | will produce SARIF results that can be uploaded to the GitHub Code Scanning Alerts experience and/or included as 5 | an artifact to view locally in the Sarif Viewer VSCode Extension. 6 | 7 | ## Usage 8 | 9 | ### Pre-requisites 10 | 11 | Include a workflow `.yml` file using an [example](#example) below as a template. Run the `msvc-code-analysis-action` 12 | after configuring CMake for your project. Building the project is only required if the C++ source files involve the use 13 | of generated files. 14 | 15 | 16 | ### Input Parameters 17 | 18 | Description of all input parameters: [action.yml](https://github.com/microsoft/msvc-code-analysis-action/blob/main/action.yml) 19 | 20 | ### Example 21 | 22 | ```yml 23 | env: 24 | # Path to the CMake build directory. 25 | build: '${{ github.workspace }}/build' 26 | config: 'Debug' 27 | 28 | jobs: 29 | analyze: 30 | name: Analyze 31 | runs-on: windows-latest 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v2 36 | 37 | - name: Configure CMake 38 | run: cmake -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} 39 | 40 | # Build is not required unless generated source files are used 41 | # - name: Build CMake 42 | # run: cmake --build ${{ env.build }} --config ${{ env.config }} 43 | 44 | - name: Run MSVC Code Analysis 45 | uses: microsoft/msvc-code-analysis-action@v0.1.1 46 | # Provide a unique ID to access the sarif output path 47 | id: run-analysis 48 | with: 49 | cmakeBuildDirectory: ${{ env.build }} 50 | buildConfiguration: ${{ env.config }} 51 | # Ruleset file that will determine what checks will be run 52 | ruleset: NativeRecommendedRules.ruleset 53 | # Paths to ignore analysis of CMake targets and includes 54 | # ignoredPaths: ${{ github.workspace }}/dependencies;${{ github.workspace }}/test 55 | 56 | # Upload SARIF file to GitHub Code Scanning Alerts 57 | - name: Upload SARIF to GitHub 58 | uses: github/codeql-action/upload-sarif@v2 59 | with: 60 | sarif_file: ${{ steps.run-analysis.outputs.sarif }} 61 | 62 | # Upload SARIF file as an Artifact to download and view 63 | - name: Upload SARIF as an Artifact 64 | uses: actions/upload-artifact@v2 65 | with: 66 | name: sarif-file 67 | path: ${{ steps.run-analysis.outputs.sarif }} 68 | ``` 69 | 70 | ### Warning Configuration 71 | 72 | By the default the action will use the set of warnings on by default inside of Visual Studio. However the tool can be 73 | configured to use any Ruleset either shipped with Visual Studio or user defined. For the best results it is 74 | recommended to use a custom Ruleset that adds/removes warnings on-top an existing Ruleset. This ensures that the user 75 | does not miss out on any new warnings are created. Refer to the 76 | [documentation on Rulesets](https://docs.microsoft.com/cpp/code-quality/using-rule-sets-to-specify-the-cpp-rules-to-run) 77 | for more information. 78 | 79 | #### Example Ruleset 80 | 81 | ```xml 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | ``` 93 | 94 | #### Suppression 95 | 96 | Ruleset are the main form of configuration but for a lightweight approach to suppress warnings you can pass options 97 | directly to the compiler. 98 | 99 | ```yml 100 | id: run-analysis 101 | with: 102 | additionalArgs: /wd6001 /wd6011 # Suppress C6001 & C6011 103 | # .... 104 | ``` 105 | 106 | ## Contributing 107 | 108 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 109 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 110 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 111 | 112 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 113 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 114 | provided by the bot. You will only need to do this once across all repos using our CLA. 115 | 116 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 117 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 118 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 119 | 120 | ## Trademarks 121 | 122 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 123 | trademarks or logos is subject to and must follow 124 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 125 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 126 | Any use of third-party trademarks or logos are subject to those third-party's policies. 127 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Microsoft C++ Code Analysis Action' 2 | description: 'Run Microsoft C++ Code Analysis to produce SARIF files for use in github/codeql-action/upload-sarif@v1' 3 | inputs: 4 | cmakeBuildDirectory: 5 | description: 'The CMake build directory that should already be generated.' 6 | required: true 7 | buildConfiguration: 8 | description: 'The build Configuration (Release, Debug, etc.) to use. This is required if using a 9 | multi-configuration CMake generator.' 10 | required: false 11 | ruleset: 12 | description: 'Ruleset file used to determine what checks are run.' 13 | default: 'NativeRecommendedRules.ruleset' 14 | ignoredPaths: 15 | description: 'Identical to setting "ignoredTargetPaths" and "ignoreSystemHeaders" for the given path. This 16 | is recommended over either option seperately.' 17 | ignoredTargetPaths: 18 | description: 'Any CMake targets defined inside these paths will be excluded from analysis. This is useful 19 | for excluding tests or locally built dependencies. List is ";" seperated, requires complete 20 | directory paths and can be absolute or relative to "github.workspace"' 21 | required: false 22 | ignoredIncludePaths: 23 | description: 'Any includes contained inside these path will be excluded from analysis. This will only filter 24 | existing paths add not add any additional includes to the compiler. This is useful for excluding 25 | target includes or other custom includes added to CMake. List is ";" seperated, requires complete 26 | directory paths and can be absolute or relative to "github.workspace"' 27 | required: false 28 | ignoreSystemHeaders: 29 | description: 'Uses /external arguments to ignore warnings from any headers marked as SYSTEM in CMake.' 30 | default: true 31 | resultsPath: 32 | description: 'Optional path to generate the SARIF file to. If not supplied "results.sarif" will be created in 33 | the CMake build directory. Path can be absolute or relative to "github.workspace".' 34 | required: false 35 | loadImplicitCompilerEnv: 36 | description: 'Load implicit includes/libs for the given MSVC toolset using Visual Studio Command Prompt. Set to 37 | false if already loaded or a custom include path is needed.' 38 | default: true 39 | additionalArgs: 40 | description: 'Optional parameters to pass to every instance of the compiler.' 41 | required: false 42 | outputs: 43 | sarif: 44 | description: 'The path to the SARIF file that is generated containing all the results.' 45 | 46 | runs: 47 | using: 'node16' 48 | main: 'dist/index.js' 49 | -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/http-client 26 | MIT 27 | Actions Http Client for Node.js 28 | 29 | Copyright (c) GitHub, Inc. 30 | 31 | All rights reserved. 32 | 33 | MIT License 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 36 | associated documentation files (the "Software"), to deal in the Software without restriction, 37 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 38 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 39 | subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 44 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 45 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 46 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 47 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 48 | 49 | 50 | @actions/io 51 | MIT 52 | The MIT License (MIT) 53 | 54 | Copyright 2019 GitHub 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 61 | 62 | balanced-match 63 | MIT 64 | (MIT) 65 | 66 | Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of 69 | this software and associated documentation files (the "Software"), to deal in 70 | the Software without restriction, including without limitation the rights to 71 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 72 | of the Software, and to permit persons to whom the Software is furnished to do 73 | so, subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 81 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 83 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 84 | SOFTWARE. 85 | 86 | 87 | brace-expansion 88 | MIT 89 | MIT License 90 | 91 | Copyright (c) 2013 Julian Gruber 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy 94 | of this software and associated documentation files (the "Software"), to deal 95 | in the Software without restriction, including without limitation the rights 96 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 97 | copies of the Software, and to permit persons to whom the Software is 98 | furnished to do so, subject to the following conditions: 99 | 100 | The above copyright notice and this permission notice shall be included in all 101 | copies or substantial portions of the Software. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 106 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 108 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 109 | SOFTWARE. 110 | 111 | 112 | concat-map 113 | MIT 114 | This software is released under the MIT license: 115 | 116 | Permission is hereby granted, free of charge, to any person obtaining a copy of 117 | this software and associated documentation files (the "Software"), to deal in 118 | the Software without restriction, including without limitation the rights to 119 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 120 | the Software, and to permit persons to whom the Software is furnished to do so, 121 | subject to the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be included in all 124 | copies or substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 127 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 128 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 129 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 130 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 131 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 132 | 133 | 134 | fs.realpath 135 | ISC 136 | The ISC License 137 | 138 | Copyright (c) Isaac Z. Schlueter and Contributors 139 | 140 | Permission to use, copy, modify, and/or distribute this software for any 141 | purpose with or without fee is hereby granted, provided that the above 142 | copyright notice and this permission notice appear in all copies. 143 | 144 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 145 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 146 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 147 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 148 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 149 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 150 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 151 | 152 | ---- 153 | 154 | This library bundles a version of the `fs.realpath` and `fs.realpathSync` 155 | methods from Node.js v0.10 under the terms of the Node.js MIT license. 156 | 157 | Node's license follows, also included at the header of `old.js` which contains 158 | the licensed code: 159 | 160 | Copyright Joyent, Inc. and other Node contributors. 161 | 162 | Permission is hereby granted, free of charge, to any person obtaining a 163 | copy of this software and associated documentation files (the "Software"), 164 | to deal in the Software without restriction, including without limitation 165 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 166 | and/or sell copies of the Software, and to permit persons to whom the 167 | Software is furnished to do so, subject to the following conditions: 168 | 169 | The above copyright notice and this permission notice shall be included in 170 | all copies or substantial portions of the Software. 171 | 172 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 173 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 174 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 175 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 176 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 177 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 178 | DEALINGS IN THE SOFTWARE. 179 | 180 | 181 | glob 182 | ISC 183 | The ISC License 184 | 185 | Copyright (c) Isaac Z. Schlueter and Contributors 186 | 187 | Permission to use, copy, modify, and/or distribute this software for any 188 | purpose with or without fee is hereby granted, provided that the above 189 | copyright notice and this permission notice appear in all copies. 190 | 191 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 192 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 193 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 194 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 195 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 196 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 197 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 198 | 199 | ## Glob Logo 200 | 201 | Glob's logo created by Tanya Brassie , licensed 202 | under a Creative Commons Attribution-ShareAlike 4.0 International License 203 | https://creativecommons.org/licenses/by-sa/4.0/ 204 | 205 | 206 | inflight 207 | ISC 208 | The ISC License 209 | 210 | Copyright (c) Isaac Z. Schlueter 211 | 212 | Permission to use, copy, modify, and/or distribute this software for any 213 | purpose with or without fee is hereby granted, provided that the above 214 | copyright notice and this permission notice appear in all copies. 215 | 216 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 217 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 218 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 219 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 220 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 221 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 222 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 223 | 224 | 225 | inherits 226 | ISC 227 | The ISC License 228 | 229 | Copyright (c) Isaac Z. Schlueter 230 | 231 | Permission to use, copy, modify, and/or distribute this software for any 232 | purpose with or without fee is hereby granted, provided that the above 233 | copyright notice and this permission notice appear in all copies. 234 | 235 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 236 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 237 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 238 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 239 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 240 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 241 | PERFORMANCE OF THIS SOFTWARE. 242 | 243 | 244 | 245 | minimatch 246 | ISC 247 | The ISC License 248 | 249 | Copyright (c) Isaac Z. Schlueter and Contributors 250 | 251 | Permission to use, copy, modify, and/or distribute this software for any 252 | purpose with or without fee is hereby granted, provided that the above 253 | copyright notice and this permission notice appear in all copies. 254 | 255 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 256 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 257 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 258 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 259 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 260 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 261 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 262 | 263 | 264 | once 265 | ISC 266 | The ISC License 267 | 268 | Copyright (c) Isaac Z. Schlueter and Contributors 269 | 270 | Permission to use, copy, modify, and/or distribute this software for any 271 | purpose with or without fee is hereby granted, provided that the above 272 | copyright notice and this permission notice appear in all copies. 273 | 274 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 275 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 276 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 277 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 278 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 279 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 280 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 281 | 282 | 283 | path-is-absolute 284 | MIT 285 | The MIT License (MIT) 286 | 287 | Copyright (c) Sindre Sorhus (sindresorhus.com) 288 | 289 | Permission is hereby granted, free of charge, to any person obtaining a copy 290 | of this software and associated documentation files (the "Software"), to deal 291 | in the Software without restriction, including without limitation the rights 292 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 293 | copies of the Software, and to permit persons to whom the Software is 294 | furnished to do so, subject to the following conditions: 295 | 296 | The above copyright notice and this permission notice shall be included in 297 | all copies or substantial portions of the Software. 298 | 299 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 300 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 301 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 302 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 303 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 304 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 305 | THE SOFTWARE. 306 | 307 | 308 | rimraf 309 | ISC 310 | The ISC License 311 | 312 | Copyright (c) Isaac Z. Schlueter and Contributors 313 | 314 | Permission to use, copy, modify, and/or distribute this software for any 315 | purpose with or without fee is hereby granted, provided that the above 316 | copyright notice and this permission notice appear in all copies. 317 | 318 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 319 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 320 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 321 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 322 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 323 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 324 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 325 | 326 | 327 | tmp 328 | MIT 329 | The MIT License (MIT) 330 | 331 | Copyright (c) 2014 KARASZI István 332 | 333 | Permission is hereby granted, free of charge, to any person obtaining a copy 334 | of this software and associated documentation files (the "Software"), to deal 335 | in the Software without restriction, including without limitation the rights 336 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 337 | copies of the Software, and to permit persons to whom the Software is 338 | furnished to do so, subject to the following conditions: 339 | 340 | The above copyright notice and this permission notice shall be included in all 341 | copies or substantial portions of the Software. 342 | 343 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 344 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 345 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 346 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 347 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 348 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 349 | SOFTWARE. 350 | 351 | 352 | tunnel 353 | MIT 354 | The MIT License (MIT) 355 | 356 | Copyright (c) 2012 Koichi Kobayashi 357 | 358 | Permission is hereby granted, free of charge, to any person obtaining a copy 359 | of this software and associated documentation files (the "Software"), to deal 360 | in the Software without restriction, including without limitation the rights 361 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 362 | copies of the Software, and to permit persons to whom the Software is 363 | furnished to do so, subject to the following conditions: 364 | 365 | The above copyright notice and this permission notice shall be included in 366 | all copies or substantial portions of the Software. 367 | 368 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 369 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 370 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 371 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 372 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 373 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 374 | THE SOFTWARE. 375 | 376 | 377 | uuid 378 | MIT 379 | The MIT License (MIT) 380 | 381 | Copyright (c) 2010-2020 Robert Kieffer and other contributors 382 | 383 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 384 | 385 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 386 | 387 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 388 | 389 | 390 | wrappy 391 | ISC 392 | The ISC License 393 | 394 | Copyright (c) Isaac Z. Schlueter and Contributors 395 | 396 | Permission to use, copy, modify, and/or distribute this software for any 397 | purpose with or without fee is hereby granted, provided that the above 398 | copyright notice and this permission notice appear in all copies. 399 | 400 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 401 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 402 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 403 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 404 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 405 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 406 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 407 | -------------------------------------------------------------------------------- /dist/vc_env.bat: -------------------------------------------------------------------------------- 1 | @CALL %1 %2 -vcvars_ver=%3 2 | @ECHO INCLUDE=%INCLUDE% 3 | @ECHO LIB=%LIB% -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const core = require('@actions/core'); 4 | const exec = require('@actions/exec'); 5 | const fs = require('fs'); 6 | const io = require('@actions/io'); 7 | const path = require('path'); 8 | const tmp = require('tmp'); 9 | const toolrunner = require('@actions/exec/lib/toolrunner'); 10 | 11 | const CMakeApiClientName = "client-msvc-ca-action"; 12 | // Paths relative to absolute path to cl.exe 13 | const RelativeRulesetPath = '..\\..\\..\\..\\..\\..\\..\\..\\Team Tools\\Static Analysis Tools\\Rule Sets'; 14 | const RelativeToolsetPath = '..\\..\\..\\..'; 15 | const RelativeCommandPromptPath = '..\\..\\..\\..\\..\\..\\..\\Auxiliary\\Build\\vcvarsall.bat'; 16 | 17 | /** 18 | * Validate if the given directory both exists and is non-empty. 19 | * @param {string} targetDir directory to test 20 | * @returns {boolean} true if the directory is empty 21 | */ 22 | function isDirectoryEmpty(targetDir) { 23 | return !targetDir || !fs.existsSync(targetDir) || (fs.readdirSync(targetDir).length) == 0; 24 | } 25 | 26 | /** 27 | * Validate if the targetDir is either equal or a sub-directory of any path in parentDirs 28 | * @param {string[]} parentDirs parent directories 29 | * @param {string} targetDir directory to test 30 | * @returns {boolean} true if a sub-directory is found 31 | */ 32 | function containsSubdirectory(parentDirs, targetDir) { 33 | const normalizedTarget = path.normalize(targetDir); 34 | return parentDirs.some((parentDir) => normalizedTarget.startsWith(path.normalize(parentDir))); 35 | } 36 | 37 | /** 38 | * Get normalized relative path from a given file/directory. 39 | * @param {string} fromPath path to join relative path to 40 | * @param {string} relativePath relative path to append 41 | * @returns normalized path 42 | */ 43 | function getRelativeTo(fromPath, relativePath) { 44 | return path.normalize(path.join(fromPath, relativePath)) 45 | } 46 | 47 | /** 48 | * Validate and resolve path by making non-absolute paths relative to GitHub 49 | * repository root. 50 | * @param {string} unresolvedPath path to resolve 51 | * @returns the resolved absolute path 52 | */ 53 | function resolvePath(unresolvedPath) { 54 | return path.normalize(path.isAbsolute(unresolvedPath) ? 55 | unresolvedPath : path.join(process.env.GITHUB_WORKSPACE, unresolvedPath)); 56 | } 57 | 58 | /** 59 | * Validate and resolve action input path by making non-absolute paths relative to 60 | * GitHub repository root. 61 | * @param {string} input name of GitHub action input variable 62 | * @param {boolean} required if true the input must be non-empty 63 | * @returns the absolute path to the input path if specified 64 | */ 65 | function resolveInputPath(input, required = false) { 66 | let inputPath = core.getInput(input); 67 | if (!inputPath) { 68 | if (required) { 69 | throw new Error(input + " input path can not be empty."); 70 | } 71 | 72 | return undefined; 73 | } 74 | 75 | return resolvePath(inputPath, required); 76 | } 77 | 78 | /** 79 | * Validate and resolve action input paths making non-absolute paths relative to 80 | * GitHub repository root. Paths are seperated by the provided string. 81 | * @param {string} input name of GitHub action input variable 82 | * @param {boolean} required if true the input must be non-empty 83 | * @returns the absolute path to the input path if specified 84 | */ 85 | function resolveInputPaths(input, required = false, seperator = ';') { 86 | const inputPaths = core.getInput(input); 87 | if (!inputPaths) { 88 | if (required) { 89 | throw new Error(input + " input paths can not be empty."); 90 | } 91 | 92 | return []; 93 | } 94 | 95 | return inputPaths.split(seperator) 96 | .map((inputPath) => resolvePath(inputPath)) 97 | .filter((inputPath) => inputPath); 98 | } 99 | 100 | /** 101 | * Create a query file for the CMake API 102 | * @param {string} apiDir CMake API directory '.cmake/api/v1' 103 | */ 104 | async function createApiQuery(apiDir) { 105 | const queryDir = path.join(apiDir, "query", CMakeApiClientName); 106 | if (!fs.existsSync(queryDir)) { 107 | await io.mkdirP(queryDir); 108 | } 109 | 110 | const queryFile = path.join(queryDir, "query.json"); 111 | const queryData = { 112 | "requests": [ 113 | { kind: "codemodel", version: 2 }, 114 | { kind: "toolchains", version: 1 } 115 | ]}; 116 | 117 | try { 118 | fs.writeFileSync(queryFile, JSON.stringify(queryData), 'utf-8'); 119 | } catch (err) { 120 | throw new Error("Failed to write query.json file for CMake API.", err); 121 | } 122 | } 123 | 124 | /** 125 | * Read and parse the given JSON reply file. 126 | * @param {string} replyFile absolute path to JSON reply 127 | * @returns parsed JSON data of the reply file 128 | */ 129 | function parseReplyFile(replyFile) { 130 | if (!fs.existsSync(replyFile)) { 131 | throw new Error("Failed to find CMake API reply file: " + replyFile); 132 | } 133 | 134 | let jsonData = fs.readFileSync(replyFile, (err) => { 135 | if (err) { 136 | throw new Error("Failed to read CMake API reply file: " + replyFile, err); 137 | } 138 | }); 139 | 140 | return JSON.parse(jsonData); 141 | } 142 | 143 | /** 144 | * Get the JSON filepath for the given response kind. 145 | * @param {string} replyDir CMake API directory for replies '.cmake/api/v1/reply' 146 | * @param {object} indexReply parsed JSON data from index-xxx.json reply 147 | * @param {string} kind the kind of response to search for 148 | * @returns the absolute path to the JSON response file, null if not found 149 | */ 150 | function getResponseFilepath(replyDir, clientResponses, kind) { 151 | const response = clientResponses.find((response) => response["kind"] == kind); 152 | return response ? path.join(replyDir, response.jsonFile) : null; 153 | } 154 | 155 | /** 156 | * Information extracted from CMake API index reply which details all other requested responses. 157 | * @param {string} replyDir CMake API directory for replies '.cmake/api/v1/reply' 158 | * @param {object} indexReply parsed JSON data from index-xxx.json reply 159 | */ 160 | function ReplyIndexInfo(replyDir, indexReply) { 161 | const clientResponses = indexReply.reply[CMakeApiClientName]["query.json"].responses; 162 | this.codemodelResponseFile = getResponseFilepath(replyDir, clientResponses, "codemodel"); 163 | this.toolchainsResponseFile = getResponseFilepath(replyDir, clientResponses, "toolchains"); 164 | this.version = indexReply.cmake.version.string; 165 | } 166 | 167 | /** 168 | * Load the information needed from the reply index file for the CMake API 169 | * @param {string} apiDir CMake API directory '.cmake/api/v1' 170 | * @returns ReplyIndexInfo info extracted from index-xxx.json reply 171 | */ 172 | function getApiReplyIndex(apiDir) { 173 | const replyDir = path.join(apiDir, "reply"); 174 | 175 | let indexFilepath; 176 | if (fs.existsSync(replyDir)) { 177 | for (const filename of fs.readdirSync(replyDir)) { 178 | if (filename.startsWith("index-")) { 179 | // get the most recent index query file (ordered lexicographically) 180 | const filepath = path.join(replyDir, filename); 181 | if (!indexFilepath || filepath > indexFilepath) { 182 | indexFilepath = filepath; 183 | } 184 | }; 185 | } 186 | } 187 | 188 | if (!indexFilepath) { 189 | throw new Error("Failed to find CMake API index reply file."); 190 | } 191 | 192 | const indexReply = parseReplyFile(indexFilepath); 193 | const replyIndexInfo = new ReplyIndexInfo(replyDir, indexReply); 194 | 195 | core.info(`Loaded '${indexFilepath}' reply generated from CMake API.`); 196 | 197 | return replyIndexInfo; 198 | } 199 | 200 | /** 201 | * Load reply data from the CMake API. This will: 202 | * - Create a query file in cmake API directory requesting data needed 203 | * - Re-run CMake on build directory to generate reply data 204 | * - Extract required information from the index-xxx.json reply 205 | * - Validate the version of CMake to ensure required reply data exists 206 | * @param {string} buildRoot build directory of CMake project 207 | * @return ReplyIndexInfo info extracted from index-xxx.json reply 208 | */ 209 | async function loadCMakeApiReplies(buildRoot) { 210 | if (isDirectoryEmpty(buildRoot)) { 211 | throw new Error("CMake build root must exist, be non-empty and be configured with CMake"); 212 | } 213 | 214 | // validate CMake can be found on the PATH 215 | await io.which("cmake", true); 216 | 217 | // create CMake API query file for the generation of replies needed 218 | const apiDir = path.join(buildRoot, ".cmake/api/v1"); 219 | await createApiQuery(apiDir); 220 | 221 | // regenerate CMake build directory to acquire CMake file API reply 222 | core.info(`Running CMake to generate reply data.`); 223 | try { 224 | await exec.exec("cmake", [ buildRoot ]) 225 | } catch (err) { 226 | throw new Error(`CMake failed to reconfigure project with error: ${err}`); 227 | } 228 | 229 | // load reply index generated from the CMake Api 230 | const replyIndexInfo = getApiReplyIndex(apiDir); 231 | if (replyIndexInfo.version < "3.20.5") { 232 | throw new Error("Action requires CMake version >= 3.20.5"); 233 | } 234 | 235 | return replyIndexInfo; 236 | } 237 | 238 | /** 239 | * Information on compiler include path. 240 | * @param {string} path the absolute path to the include directory 241 | * @param {boolean} isSystem true if this should be treated as a CMake SYSTEM path 242 | */ 243 | function IncludePath(path, isSystem) { 244 | this.path = path; 245 | this.isSystem = isSystem; 246 | } 247 | 248 | /** 249 | * Information about the language and compiler being used to compile a source file. 250 | * @param {object} toolchain ReplyIndexInfo info extracted from index-xxx.json reply 251 | */ 252 | function ToolchainInfo(toolchain) { 253 | this.language = toolchain.language; 254 | this.path = toolchain.compiler.path; 255 | this.version = toolchain.compiler.version; 256 | this.includes = (toolchain.compiler.implicit.includeDirectories || []).map( 257 | (include) => new IncludePath(include, true)); 258 | 259 | // extract toolset-version & host/target arch from folder layout in VS 260 | this.toolsetVersion = path.basename(getRelativeTo(this.path, RelativeToolsetPath)); 261 | const targetDir = path.dirname(this.path); 262 | const hostDir = path.dirname(targetDir); 263 | this.targetArch = path.basename(targetDir); 264 | switch (path.basename(hostDir).toLowerCase()) { 265 | case 'hostx86': 266 | this.hostArch = 'x86'; 267 | break; 268 | case 'hostx64': 269 | this.hostArch = 'x64'; 270 | break; 271 | default: 272 | throw new Error('Unknown MSVC toolset layout'); 273 | } 274 | } 275 | 276 | /** 277 | * Parse the toolchain-xxx.json file to find information on any MSVC toolchains used. If none are 278 | * found issue an error. 279 | * @param {ReplyIndexInfo} replyIndexInfo ReplyIndexInfo info extracted from index-xxx.json reply 280 | * @returns Toolchain info extracted from toolchain-xxx.json 281 | */ 282 | function loadToolchainMap(replyIndexInfo) { 283 | if (!fs.existsSync(replyIndexInfo.toolchainsResponseFile)) { 284 | throw new Error("Failed to load toolchains response from CMake API"); 285 | } 286 | 287 | const toolchainMap = {}; 288 | const toolchains = parseReplyFile(replyIndexInfo.toolchainsResponseFile); 289 | const cToolchain = toolchains.toolchains.find( 290 | (t) => t.language == "C" && t.compiler.id == "MSVC"); 291 | if (cToolchain) { 292 | toolchainMap[cToolchain.language] = new ToolchainInfo(cToolchain); 293 | } 294 | 295 | const cxxToolchain = toolchains.toolchains.find( 296 | (t) => t.language == "CXX" && t.compiler.id == "MSVC"); 297 | if (cxxToolchain) { 298 | toolchainMap[cxxToolchain.language] = new ToolchainInfo(cxxToolchain); 299 | } 300 | 301 | 302 | if (Object.keys(toolchainMap).length === 0) { 303 | throw new Error("Action requires use of MSVC for either/both C or C++."); 304 | } 305 | 306 | return toolchainMap; 307 | } 308 | 309 | /** 310 | * Information on each compilation unit extracted from the CMake targets. 311 | * @param {object} group compilation data shared between one or more source files 312 | * @param {string} source absolute path to source file being compiled 313 | */ 314 | function CompileCommand(group, source) { 315 | // Filepath to source file being compiled 316 | this.source = source; 317 | // Compiler language used 318 | this.language = group.language; 319 | // C++ Standard 320 | this.standard = group.languageStandard ? group.languageStandard.standard : undefined; 321 | // Compile command line fragments appended into a single string 322 | this.args = (group.compileCommandFragments || []).map((c) => c.fragment).join(" "); 323 | // includes, both regular and system 324 | this.includes = (group.includes || []).map((inc) => 325 | new IncludePath(inc.path, inc.isSystem || false)); 326 | // defines 327 | this.defines = (group.defines || []).map((d) => d.define); 328 | } 329 | 330 | /** 331 | * Parse the codemodel-xxx.json and each target-xxx.json to find information on required to compile 332 | * each source file in the project. 333 | * @param {ReplyIndexInfo} replyIndexInfo ReplyIndexInfo info extracted from index-xxx.json reply 334 | * @returns CompileCommand information for each compiled source file in the project 335 | */ 336 | function loadCompileCommands(replyIndexInfo, buildConfiguration, excludedTargetPaths) { 337 | if (!fs.existsSync(replyIndexInfo.codemodelResponseFile)) { 338 | throw new Error("Failed to load codemodel response from CMake API"); 339 | } 340 | 341 | let compileCommands = []; 342 | const codemodel = parseReplyFile(replyIndexInfo.codemodelResponseFile); 343 | const sourceRoot = codemodel.paths.source; 344 | const replyDir = path.dirname(replyIndexInfo.codemodelResponseFile); 345 | let configurations = codemodel.configurations; 346 | if (configurations.length > 1) { 347 | if (!buildConfiguration) { 348 | throw new Error("buildConfiguration is required for multi-config CMake Generators."); 349 | } 350 | 351 | configurations = configurations.filter((config) => buildConfiguration == config.name); 352 | if (configurations.length == 0) { 353 | throw new Error("buildConfiguration does not match any available in CMake project."); 354 | } 355 | } else if (buildConfiguration && configurations[0].name != buildConfiguration) { 356 | throw new Error(`buildConfiguration does not match '${configurations[0].name}' configuration used by CMake.`); 357 | } 358 | 359 | const codemodelInfo = configurations[0]; 360 | for (const targetInfo of codemodelInfo.targets) { 361 | const targetDir = path.join(sourceRoot, codemodelInfo.directories[targetInfo.directoryIndex].source); 362 | if (containsSubdirectory(excludedTargetPaths, targetDir)) { 363 | continue; 364 | } 365 | 366 | const target = parseReplyFile(path.join(replyDir, targetInfo.jsonFile)); 367 | for (const group of target.compileGroups || []) { 368 | for (const sourceIndex of group.sourceIndexes) { 369 | const source = path.join(sourceRoot, target.sources[sourceIndex].path); 370 | compileCommands.push(new CompileCommand(group, source)); 371 | } 372 | } 373 | } 374 | 375 | return compileCommands; 376 | } 377 | 378 | /** 379 | * Find path to EspXEngine.dll as it only exists in host/target bin for MSVC Visual Studio release. 380 | * @param {ToolchainInfo} toolchain information on the toolchain being used 381 | * @returns absolute path to EspXEngine.dll 382 | */ 383 | function findEspXEngine(toolchain) { 384 | const hostDir = path.dirname(path.dirname(toolchain.path)); 385 | const espXEnginePath = path.join(hostDir, toolchain.hostArch, 'EspXEngine.dll'); 386 | if (fs.existsSync(espXEnginePath)) { 387 | return espXEnginePath; 388 | } 389 | 390 | throw new Error(`Unable to find: ${espXEnginePath}`); 391 | } 392 | 393 | /** 394 | * Find official ruleset directory using the known path of MSVC compiler in Visual Studio. 395 | * @param {ToolchainInfo} toolchain information on the toolchain being used 396 | * @returns absolute path to directory containing all Visual Studio rulesets 397 | */ 398 | function findRulesetDirectory(toolchain) { 399 | const rulesetDirectory = getRelativeTo(toolchain.path, RelativeRulesetPath); 400 | return fs.existsSync(rulesetDirectory) ? rulesetDirectory : undefined; 401 | } 402 | 403 | /** 404 | * Find ruleset first searching relative to GitHub repository and then relative to the official ruleset directory 405 | * shipped in Visual Studio. 406 | * @param {string} rulesetDirectory path to directory containing all Visual Studio rulesets 407 | * @returns path to ruleset found locally or inside Visual Studio 408 | */ 409 | function findRuleset(rulesetDirectory) { 410 | const repoRulesetPath = resolveInputPath("ruleset"); 411 | if (!repoRulesetPath) { 412 | return undefined; 413 | } else if (fs.existsSync(repoRulesetPath)) { 414 | core.info(`Found local ruleset: ${repoRulesetPath}`); 415 | return repoRulesetPath; 416 | } 417 | 418 | // search official ruleset directory that ships inside of Visual Studio 419 | const rulesetPath = core.getInput("ruleset"); 420 | if (rulesetDirectory != undefined) { 421 | const officialRulesetPath = path.join(rulesetDirectory, rulesetPath); 422 | if (fs.existsSync(officialRulesetPath)) { 423 | core.info(`Found official ruleset: ${officialRulesetPath}`); 424 | return officialRulesetPath; 425 | } 426 | } else { 427 | core.warning("Unable to find official rulesets shipped with Visual Studio."); 428 | } 429 | 430 | throw new Error(`Unable to find local or official ruleset specified: ${rulesetPath}`); 431 | } 432 | 433 | /** 434 | * Options to enable/disable different compiler features. 435 | */ 436 | function CompilerCommandOptions() { 437 | // Build configuration to use when using a multi-config CMake generator. 438 | this.buildConfiguration = core.getInput("buildConfiguration"); 439 | // Use /external command line options to ignore warnings in CMake SYSTEM headers. 440 | this.ignoreSystemHeaders = core.getInput("ignoreSystemHeaders"); 441 | // Toggle whether implicit includes/libs are loaded from Visual Studio Command Prompt 442 | this.loadImplicitCompilerEnv = core.getInput("loadImplicitCompilerEnv"); 443 | // Ignore analysis on any CMake targets or includes. 444 | this.ignoredPaths = resolveInputPaths("ignoredPaths"); 445 | this.ignoredTargetPaths = this.ignoredPaths || []; 446 | this.ignoredTargetPaths = this.ignoredTargetPaths.concat(resolveInputPaths("ignoredTargetPaths")); 447 | this.ignoredIncludePaths = this.ignoredPaths || []; 448 | this.ignoredIncludePaths = this.ignoredIncludePaths.concat(resolveInputPaths("ignoredIncludePaths")); 449 | // Additional arguments to add the command-line of every analysis instance 450 | this.additionalArgs = core.getInput("additionalArgs"); 451 | // TODO: add support to build precompiled headers before running analysis. 452 | this.usePrecompiledHeaders = false; // core.getInput("usePrecompiledHeaders"); 453 | } 454 | 455 | /** 456 | * Construct all command-line arguments that will be common among all sources files of a given compiler. 457 | * @param {*} toolchain information on the toolchain being used 458 | * @param {CompilerCommandOptions} options options for different compiler features 459 | * @returns list of analyze arguments common to the given toolchain 460 | */ 461 | function getCommonAnalyzeArguments(toolchain, options) { 462 | const args = ["/analyze:only", "/analyze:quiet", "/analyze:log:format:sarif", "/nologo"]; 463 | 464 | const espXEngine = findEspXEngine(toolchain); 465 | args.push(`/analyze:plugin${espXEngine}`); 466 | 467 | const rulesetDirectory = findRulesetDirectory(toolchain); 468 | const rulesetPath = findRuleset(rulesetDirectory); 469 | if (rulesetPath != undefined) { 470 | args.push(`/analyze:ruleset${rulesetPath}`); 471 | 472 | // add ruleset directories incase user includes any official rulesets 473 | if (rulesetDirectory != undefined) { 474 | args.push(`/analyze:rulesetdirectory${rulesetDirectory}`); 475 | } 476 | } else { 477 | core.warning('Ruleset is not being used, all warnings will be enabled.'); 478 | } 479 | 480 | if (options.ignoreSystemHeaders) { 481 | args.push(`/external:W0`); 482 | args.push(`/analyze:external-`); 483 | } 484 | 485 | if (options.additionalArgs) { 486 | args = args.concat(toolrunner.argStringToArray(options.additionalArgs)); 487 | } 488 | 489 | return args; 490 | } 491 | 492 | /** 493 | * Extract the the implicit includes that should be used with the given compiler from the 494 | * Visual Studio command prompt corresponding with the toolchain used. This is required 495 | * as MSVC does not populate the CMake API `toolchain.implicit.includeDirectories` property. 496 | * @param {ToolchainInfo} toolchain information on the toolchain being used 497 | * @returns array of default includes used by the given MSVC toolset 498 | */ 499 | async function extractEnvironmentFromCommandPrompt(toolchain) { 500 | // use bat file to output environment variable required after running 'vcvarsall.bat' 501 | const vcEnvScript = path.join(__dirname, "vc_env.bat"); 502 | // init arguments for 'vcvarsall.bat' to match the toolset version/arch used 503 | const commandPromptPath = getRelativeTo(toolchain.path, RelativeCommandPromptPath); 504 | const arch = (toolchain.hostArch == toolchain.targetArch) ? 505 | toolchain.hostArch : `${toolchain.hostArch}_${toolchain.targetArch}`; 506 | 507 | core.info("Extracting environment from VS Command Prompt"); 508 | const execOptions = { silent: true }; 509 | const execOutput = await exec.getExecOutput(vcEnvScript, 510 | [commandPromptPath, arch, toolchain.toolsetVersion], execOptions); 511 | if (execOutput.exitCode != 0) { 512 | core.debug(execOutput.stdout); 513 | throw new Error("Failed to run VS Command Prompt to collect implicit includes/libs"); 514 | } 515 | 516 | const env = { INCLUDE: "", LIB: "" }; 517 | for (const line of execOutput.stdout.split(/\r?\n/)) { 518 | const index = line.indexOf('='); 519 | if (index != -1) { 520 | const envVar = line.substring(0, index); 521 | if (envVar in env) { 522 | env[envVar] = line.substring(index + 1); 523 | } 524 | } 525 | } 526 | 527 | return env; 528 | } 529 | 530 | /** 531 | * Construct all environment variables that will be common among all sources files of a given compiler. 532 | * @param {ToolchainInfo} toolchain information on the toolchain being used 533 | * @param {CompilerCommandOptions} options options for different compiler features 534 | * @returns map of environment variables common to the given toolchain 535 | */ 536 | async function getCommonAnalyzeEnvironment(toolchain, options) { 537 | const env = { 538 | CAEmitSarifLog: "1", // enable compatibility mode as GitHub does not support some sarif options 539 | CAExcludePath: process.env.CAExcludePath || "", 540 | INCLUDE: process.env.INCLUDE || "", 541 | LIB: process.env.LIB || "", 542 | }; 543 | 544 | if (options.loadImplicitCompilerEnv) { 545 | const commandPromptEnv = await extractEnvironmentFromCommandPrompt(toolchain); 546 | env.CAExcludePath += `;${commandPromptEnv.INCLUDE}`; // exclude all implicit includes 547 | env.INCLUDE += `;${commandPromptEnv.INCLUDE}`; 548 | env.LIB += `;${commandPromptEnv.LIB}`; 549 | } 550 | 551 | return env; 552 | } 553 | 554 | /** 555 | * Information required to run analysis on a single source file. 556 | * @param {string} source absolute path to the source file being compiled 557 | * @param {string} compiler absolute path to compiler used 558 | * @param {string[]} args all compilation and analyze arguments to pass to cl.exe 559 | * @param {[key: string]: string} env environment to use when running cl.exe 560 | * @param {string} sarifLog absolute path to SARIF log file that will be produced 561 | */ 562 | function AnalyzeCommand(source, compiler, args, env, sarifLog) { 563 | this.source = source; 564 | this.compiler = compiler; 565 | this.args = args; 566 | this.env = env; 567 | this.sarifLog = sarifLog; 568 | } 569 | 570 | /** 571 | * Load information needed to compile and analyze each source file in the given CMake project. 572 | * This makes use of the CMake file API and other sources to collect this data. 573 | * @param {string} buildRoot absolute path to the build directory of the CMake project 574 | * @param {CompilerCommandOptions} options options for different compiler features 575 | * @returns list of information to compile and analyze each source file in the project 576 | */ 577 | async function createAnalysisCommands(buildRoot, options) { 578 | const replyIndexInfo = await loadCMakeApiReplies(buildRoot); 579 | const toolchainMap = loadToolchainMap(replyIndexInfo); 580 | const compileCommands = loadCompileCommands(replyIndexInfo, options.buildConfiguration, options.ignoredTargetPaths); 581 | 582 | let commonArgsMap = {}; 583 | let commonEnvMap = {}; 584 | for (const toolchain of Object.values(toolchainMap)) { 585 | if (!(toolchain.path in commonArgsMap)) { 586 | commonArgsMap[toolchain.path] = getCommonAnalyzeArguments(toolchain, options); 587 | commonEnvMap[toolchain.path] = await getCommonAnalyzeEnvironment(toolchain, options); 588 | } 589 | } 590 | 591 | let analyzeCommands = [] 592 | for (const command of compileCommands) { 593 | const toolchain = toolchainMap[command.language]; 594 | if (toolchain) { 595 | let args = toolrunner.argStringToArray(command.args); 596 | const allIncludes = toolchain.includes.concat(command.includes); 597 | for (const include of allIncludes) { 598 | if ((options.ignoreSystemHeaders && include.isSystem) || 599 | containsSubdirectory(options.ignoredIncludePaths, include.path)) { 600 | // TODO: filter compiler versions that don't support /external. 601 | args.push(`/external:I${include.path}`); 602 | } else { 603 | args.push(`/I${include.path}`); 604 | } 605 | } 606 | 607 | for (const define of command.defines) { 608 | args.push(`/D${define}`); 609 | } 610 | 611 | args.push(command.source); 612 | 613 | let sarifLog = null; 614 | try { 615 | sarifLog = tmp.fileSync({ postfix: '.sarif', discardDescriptor: true }).name; 616 | } catch (err) { 617 | // Clean up all temp SARIF logs 618 | analyzeCommands.forEach(command => fs.unlinkSync(command.sarifLog)); 619 | throw Error(`Failed to create temporary file to write SARIF: ${err}`, err); 620 | } 621 | 622 | args.push(`/analyze:log${sarifLog}`); 623 | 624 | args = args.concat(commonArgsMap[toolchain.path]); 625 | analyzeCommands.push(new AnalyzeCommand( 626 | command.source, toolchain.path, args, commonEnvMap[toolchain.path], sarifLog)); 627 | } 628 | } 629 | 630 | return analyzeCommands; 631 | } 632 | 633 | // TODO: use a more performant data-structure such a hash-set 634 | function ResultCache() { 635 | this.files = {}; 636 | this.addIfUnique = function(sarifResult) { 637 | const id = sarifResult.ruleId; 638 | if (!id) { 639 | throw Error(`Found warning with no ID, resolve before continuing`); 640 | } 641 | 642 | const message = sarifResult.message ? sarifResult.message.text : undefined; 643 | if (!message) { 644 | throw Error(`Found warning with no message, resolve before continuing: ${id}`); 645 | } 646 | 647 | if (!sarifResult.locations || !sarifResult.locations[0] || !sarifResult.locations[0].physicalLocation) { 648 | throw Error(`Found warning with no location, resolve before continuing:\n${id}: ${message}`); 649 | } 650 | 651 | const physicalLocation = sarifResult.locations[0].physicalLocation; 652 | const file = physicalLocation.artifactLocation ? physicalLocation.artifactLocation.uri : undefined; 653 | const line = physicalLocation.region ? physicalLocation.region.startLine : undefined; 654 | const column = physicalLocation.region ? physicalLocation.region.startColumn : undefined; 655 | if (file == undefined || line == undefined || column == undefined) { 656 | throw Error(`Found warning with invalid location, resolve before continuing:\n${id}: ${message}`); 657 | } 658 | 659 | this.files[file] = this.files[file] || {}; 660 | this.files[file][id] = this.files[file][id] || []; 661 | 662 | const ruleCache = this.files[file][id]; 663 | if (ruleCache.some((result) => 664 | result.line == line && result.column == column && result.message == message)) { 665 | return false; 666 | } 667 | 668 | ruleCache.push({ 669 | line: line, 670 | column: column, 671 | message: message 672 | }); 673 | 674 | return true; 675 | }; 676 | }; 677 | 678 | function combineSarif(resultPath, sarifFiles) { 679 | const resultCache = new ResultCache(); 680 | const combinedSarif = { 681 | "version": "2.1.0", 682 | "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", 683 | "runs": [{ 684 | "tool": null, 685 | "results": [] 686 | }] 687 | }; 688 | 689 | for (const sarifFile of sarifFiles) { 690 | const sarifLog = parseReplyFile(sarifFile); 691 | for (const run of sarifLog.runs) { 692 | if (!combinedSarif.runs[0].tool) { 693 | combinedSarif.runs[0].tool = run.tool; 694 | } 695 | 696 | for (const result of run.results) { 697 | if (resultCache.addIfUnique(result)) { 698 | combinedSarif.runs[0].results.push(result); 699 | } 700 | } 701 | } 702 | } 703 | 704 | try { 705 | fs.writeFileSync(resultPath, JSON.stringify(combinedSarif), 'utf-8'); 706 | } catch (err) { 707 | throw new Error("Failed to write combined SARIF result file.", err); 708 | } 709 | } 710 | 711 | /** 712 | * Main 713 | */ 714 | async function main() { 715 | var analyzeCommands = []; 716 | try { 717 | const buildDir = resolveInputPath("cmakeBuildDirectory", true); 718 | if (!fs.existsSync(buildDir)) { 719 | throw new Error("CMake build directory does not exist. Ensure CMake is already configured."); 720 | } 721 | 722 | let resultPath = resolveInputPath("resultsPath", false); 723 | if (!resultPath) { 724 | resultPath = path.join(buildDir, "results.sarif"); 725 | } else if (!fs.existsSync(path.dirname(resultPath))) { 726 | throw new Error("Directory of the 'resultPath' file must already exist."); 727 | } 728 | 729 | const options = new CompilerCommandOptions(); 730 | analyzeCommands = await createAnalysisCommands(buildDir, options); 731 | if (analyzeCommands.length == 0) { 732 | throw new Error('No C/C++ files were found in the project that could be analyzed.'); 733 | } 734 | 735 | // TODO: parallelism 736 | const failedSourceFiles = []; 737 | for (const command of analyzeCommands) { 738 | const execOptions = { 739 | cwd: buildDir, 740 | env: command.env, 741 | }; 742 | 743 | // TODO: timeouts 744 | core.info(`Running analysis on: ${command.source}`); 745 | try { 746 | await exec.exec(`"${command.compiler}"`, command.args, execOptions); 747 | } catch (err) { 748 | core.debug(`Compilation failed with error: ${err}`); 749 | core.debug("Environment:"); 750 | core.debug(execOptions.env); 751 | failedSourceFiles.push(command.source); 752 | } 753 | } 754 | 755 | if (failedSourceFiles.length > 0) { 756 | const fileList = failedSourceFiles 757 | .map(file => path.basename(file)) 758 | .join(","); 759 | throw new Error(`Analysis failed due to compiler errors in files: ${fileList}`); 760 | } 761 | 762 | const sarifResults = analyzeCommands.map(command => command.sarifLog); 763 | combineSarif(resultPath, sarifResults); 764 | core.setOutput("sarif", resultPath); 765 | 766 | } catch (error) { 767 | if (core.isDebug()) { 768 | core.setFailed(error.stack) 769 | } else { 770 | core.setFailed(error) 771 | } 772 | } finally { 773 | analyzeCommands.map(command => command.sarifLog) 774 | .filter(log => fs.existsSync(log)) 775 | .forEach(log => fs.unlinkSync(log)); 776 | } 777 | } 778 | 779 | if (require.main === module) { 780 | (async () => { 781 | await main(); 782 | })(); 783 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "mocha" 4 | }, 5 | "dependencies": { 6 | "@actions/core": "^1.9.1", 7 | "@actions/exec": "^1.1.0", 8 | "@actions/io": "^1.1.1", 9 | "chai": "^4.3.4", 10 | "chai-as-promised": "^7.1.1", 11 | "mocha": "^9.0.3", 12 | "rewire": "^5.0.0", 13 | "testdouble": "^3.16.1", 14 | "tmp": "^0.2.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/cmake-api-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"); 4 | const chai = require("chai"); 5 | const chaiAsPromised = require("chai-as-promised"); 6 | const path = require("path"); 7 | const rewire = require("rewire"); 8 | const td = require("testdouble"); 9 | 10 | chai.use(chaiAsPromised); 11 | const expect = chai.expect; 12 | chai.should(); 13 | 14 | const cmakeExePath = path.normalize("C:\\path\\to\\cmake.exe"); 15 | const cmakeBuildDir = path.normalize("path\\to\\cmake\\build"); 16 | const cmakeSrcDir = path.normalize("path\\to\\project\\src"); 17 | 18 | const cmakeApiDir = path.join(cmakeBuildDir, path.normalize(".cmake\\api\\v1")); 19 | const cmakeQueryDir = path.join(cmakeApiDir, "query"); 20 | const cmakeReplyDir = path.join(cmakeApiDir, "reply"); 21 | 22 | const clPath = "C:/VS/root/Tools/MSVC/14.29.30133/bin/Hostx86/x86/cl.exe"; 23 | const clInclude = "C:/VS/root/Tools/MSVC/14.29.30133/include"; 24 | 25 | const PATHEnv = [ 26 | path.dirname(clPath), 27 | path.dirname(cmakeExePath) 28 | ].join(";"); 29 | 30 | const cmakeIndexReply = "index-1.json"; 31 | const cmakeCacheReply = "cache-1.json"; 32 | const cmakeCodemodelReply = "codemodel-1.json"; 33 | const cmakeToolchainsReply = "toolchains-1.json"; 34 | const cmakeTarget1Reply = "target-1.json"; 35 | const cmakeTarget2Reply = "target-2.json"; 36 | 37 | let defaultFileContents = {}; 38 | defaultFileContents[cmakeIndexReply] = { 39 | "cmake": { 40 | "generator": { 41 | "multiConfig": true, 42 | "name": "Visual Studio 17 2022" 43 | }, 44 | "paths": { 45 | "cmake": cmakeExePath 46 | }, 47 | "version": { 48 | "string": "3.21.6" 49 | }, 50 | }, 51 | "reply" : { 52 | "client-fake": { 53 | "invalid": "data" 54 | }, 55 | "client-msvc-ca-action" : { 56 | "query.json" : { 57 | "responses": [ 58 | { "kind" : "cache", "jsonFile" : cmakeCacheReply }, 59 | { "kind" : "codemodel", "jsonFile" : cmakeCodemodelReply }, 60 | { "kind" : "toolchains", "jsonFile" : cmakeToolchainsReply } 61 | ] 62 | } 63 | } 64 | } 65 | }; 66 | 67 | defaultFileContents[cmakeCodemodelReply] = { 68 | "kind": "codemodel", 69 | "paths": { 70 | "build": cmakeBuildDir, 71 | "source": cmakeSrcDir 72 | }, 73 | "configurations" : [ 74 | { 75 | "name": "Regular", 76 | "directories": [ 77 | { "source": "." }, 78 | { "source": "test" } 79 | ], 80 | "targets": [ 81 | { 82 | "directoryIndex": 0, 83 | "jsonFile": cmakeTarget1Reply 84 | }, 85 | { 86 | "directoryIndex": 0, 87 | "jsonFile": cmakeTarget2Reply 88 | } 89 | ] 90 | }, 91 | { 92 | "name": "OnlyTarget2", 93 | "directories": [ 94 | { "source": "." } 95 | ], 96 | "targets": [ 97 | { 98 | "directoryIndex": 0, 99 | "jsonFile": cmakeTarget2Reply 100 | } 101 | ] 102 | } 103 | ] 104 | }; 105 | 106 | const CLangIndex = 0; 107 | const CXXLangIndex = 1; 108 | defaultFileContents[cmakeCacheReply] = { 109 | "kind": "cache", 110 | "entries": [ 111 | { // CLangIndex 112 | "name": "CMAKE_C_COMPILER", 113 | "value": clPath 114 | }, 115 | { // CXXLangIndex 116 | "name": "CMAKE_CXX_COMPILER", 117 | "value": clPath 118 | } 119 | ] 120 | }; 121 | 122 | defaultFileContents[cmakeToolchainsReply] = { 123 | "kind": "toolchains", 124 | "toolchains": [ 125 | { // CLangIndex 126 | "language": "C", 127 | "compiler" : { 128 | "path": clPath, 129 | "id": "MSVC", 130 | "version": "14.29.30133", 131 | "implicit": { 132 | "includeDirectories": [ 133 | clInclude 134 | ] 135 | } 136 | } 137 | }, 138 | { // CXXLangIndex 139 | "language": "CXX", 140 | "compiler" : { 141 | "path": clPath, 142 | "id": "MSVC", 143 | "version": "14.29.30133", 144 | "implicit": { 145 | "includeDirectories": [ 146 | clInclude 147 | ] 148 | } 149 | } 150 | } 151 | 152 | ] 153 | }; 154 | 155 | const sharedArgs = "/a /b /c"; 156 | const uniqueArgs = "/e /f"; 157 | const totalCompileCommands = 4; 158 | defaultFileContents[cmakeTarget1Reply] = { 159 | "compileGroups": [ 160 | { 161 | "compileCommandFragments": [ 162 | { "fragment": sharedArgs }, 163 | { "fragment": uniqueArgs } 164 | ], 165 | "includes": [ 166 | { "path": "regular/include"}, 167 | { "path": "external/include", "isSystem": true } 168 | ], 169 | "language": "CXX", 170 | "sourceIndexes": [ 171 | 0, 172 | 2 173 | ] 174 | }, 175 | { 176 | "compileCommandFragments": [ 177 | { "fragment": sharedArgs } 178 | ], 179 | "includes": [ 180 | { "path": "regular/include", "isSystem": false }, 181 | { "path": "external/include", "isSystem": true } 182 | ], 183 | "language": "C", 184 | "sourceIndexes": [ 185 | 1 186 | ] 187 | }, 188 | ], 189 | "sources": [ 190 | { "path" : "src/file1.cpp"}, 191 | { "path" : "src/file2.c"}, 192 | { "path" : "src/file3.cxx"}, 193 | ] 194 | }; 195 | 196 | defaultFileContents[cmakeTarget2Reply] = { 197 | "compileGroups": [ 198 | { 199 | "compileCommandFragments": [ 200 | { "fragment": sharedArgs } 201 | ], 202 | "includes": [ 203 | { "path": "regular/include" }, 204 | { "path": "external/include", "isSystem": true } 205 | ], 206 | "defines": [ 207 | { "define": "a=b"}, 208 | { "define": "c=d"}, 209 | ], 210 | "language": "CXX", 211 | "sourceIndexes": [ 212 | 0 213 | ] 214 | }, 215 | ], 216 | "sources": [ 217 | { "path" : "src/file4.cpp"}, 218 | ] 219 | }; 220 | 221 | describe("CMakeApi", () => { 222 | let action; 223 | let exec; 224 | let fs; 225 | let io; 226 | 227 | let getApiReplyIndex; 228 | let loadCMakeApiReplies; 229 | let loadToolchainMap; 230 | let loadCompileCommands; 231 | 232 | function setReplyContents(filename) { 233 | const filepath = path.join(cmakeReplyDir, filename); 234 | td.when(fs.existsSync(filepath)).thenReturn(true); 235 | td.when(fs.readFileSync(filepath, td.matchers.anything())).thenReturn( 236 | JSON.stringify(defaultFileContents[filename])); 237 | } 238 | 239 | function editReplyContents(filename, editCallback) { 240 | const filepath = path.join(cmakeReplyDir, filename); 241 | let contents = JSON.parse(JSON.stringify(defaultFileContents[filename])); 242 | editCallback(contents); 243 | td.when(fs.readFileSync(filepath, td.matchers.anything())).thenReturn(JSON.stringify(contents)); 244 | } 245 | 246 | function validateCompileCommands(compileCommands) { 247 | for (const command of compileCommands) { 248 | command.args.should.contain(sharedArgs); 249 | command.includes.length.should.equal(2); 250 | switch (path.basename(command.source)) { 251 | case "file1.cpp": 252 | case "file3.cxx": 253 | command.args.should.contain(uniqueArgs); 254 | break; 255 | case "file2.c": 256 | break; 257 | case "file4.cpp": 258 | command.defines.should.contain("a=b"); 259 | command.defines.should.contain("c=d"); 260 | break; 261 | default: 262 | assert.fail("Unknown source file: " + compileCommand.source); 263 | } 264 | } 265 | } 266 | 267 | beforeEach(() => { 268 | // modules 269 | td.replace('@actions/core'); 270 | exec = td.replace('@actions/exec'); 271 | fs = td.replace("fs"); 272 | io = td.replace('@actions/io'); 273 | action = rewire("../index.js"); 274 | 275 | getApiReplyIndex = action.__get__("getApiReplyIndex"); 276 | loadCMakeApiReplies = action.__get__("loadCMakeApiReplies"); 277 | loadToolchainMap = action.__get__("loadToolchainMap"); 278 | loadCompileCommands = action.__get__("loadCompileCommands"); 279 | 280 | // default cmake folders 281 | td.when(fs.existsSync(cmakeBuildDir)).thenReturn(true); 282 | td.when(fs.existsSync(cmakeSrcDir)).thenReturn(true); 283 | td.when(fs.existsSync(cmakeApiDir)).thenReturn(true); 284 | td.when(fs.existsSync(cmakeQueryDir)).thenReturn(true); 285 | td.when(fs.existsSync(cmakeReplyDir)).thenReturn(true); 286 | // cmakeBuildDir must be non-empty 287 | td.when(fs.readdirSync(cmakeBuildDir)).thenReturn([".cmake"]); 288 | 289 | // cmake discoverable and successfully executable 290 | td.when(io.which("cmake", true)).thenResolve(cmakeExePath); 291 | td.when(exec.exec("cmake", [cmakeBuildDir])).thenResolve(0); 292 | 293 | // default MSVC toolset 294 | td.when(fs.existsSync(clPath)).thenReturn(true); 295 | td.when(fs.existsSync(clInclude)).thenReturn(true); 296 | 297 | // default reply files 298 | td.when(fs.readdirSync(cmakeReplyDir)).thenReturn(Object.keys(defaultFileContents)); 299 | for (const filename of Object.keys(defaultFileContents)) { 300 | setReplyContents(filename); 301 | } 302 | }); 303 | 304 | afterEach(() => { 305 | td.reset(); 306 | }); 307 | 308 | // Common tests. 309 | it("loadCompileCommands", async () => { 310 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 311 | const compileCommands = loadCompileCommands(replyIndexInfo, "Regular", []); 312 | validateCompileCommands(compileCommands); 313 | compileCommands.length.should.equal(totalCompileCommands); 314 | }); 315 | 316 | it("exclude targets", async () => { 317 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 318 | const compileCommands = loadCompileCommands(replyIndexInfo, "Regular", [cmakeSrcDir]); 319 | validateCompileCommands(compileCommands); 320 | compileCommands.length.should.equal(0); 321 | }); 322 | 323 | it("filter commands with config", async () => { 324 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 325 | const compileCommands = loadCompileCommands(replyIndexInfo, "OnlyTarget2", []); 326 | validateCompileCommands(compileCommands); 327 | compileCommands.length.should.equal(1); 328 | }); 329 | 330 | it("loadToolchainMap", async () => { 331 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 332 | const toolchainMap = loadToolchainMap(replyIndexInfo); 333 | toolchainMap.should.have.keys(["C", "CXX"]); 334 | }); 335 | 336 | // only testing user errors, assume format of query/reply files is valid 337 | describe("errors", () => { 338 | it("empty buildRoot", async () => { 339 | await expect(loadCMakeApiReplies("")).to.be.rejectedWith( 340 | "CMake build root must exist, be non-empty and be configured with CMake"); 341 | }); 342 | 343 | it("buildRoot does not exist", async () => { 344 | td.when(fs.existsSync(cmakeBuildDir)).thenReturn(false); 345 | await expect(loadCMakeApiReplies(cmakeBuildDir)).to.be.rejectedWith( 346 | "CMake build root must exist, be non-empty and be configured with CMake"); 347 | }); 348 | 349 | it("cmake not on path", async () => { 350 | td.when(io.which("cmake", true)).thenReject(new Error("cmake missing")); 351 | await expect(loadCMakeApiReplies(cmakeBuildDir)).to.be.rejectedWith("cmake missing"); 352 | }); 353 | 354 | it("cmake.exe failed to run", async () => { 355 | td.when(exec.exec("cmake", td.matchers.anything())).thenReject(new Error()); 356 | await expect(loadCMakeApiReplies(cmakeBuildDir)).to.be.rejectedWith( 357 | "CMake failed to reconfigure project with error:"); 358 | }); 359 | 360 | it("cmake not run (missing .cmake/api dir)", async () => { 361 | td.when(fs.existsSync(cmakeReplyDir)).thenReturn(false); 362 | await expect(loadCMakeApiReplies(cmakeBuildDir)).to.be.rejectedWith( 363 | "Failed to find CMake API index reply file."); 364 | }); 365 | 366 | it("cmake version < 3.20.5", async () => { 367 | editReplyContents(cmakeIndexReply, (reply) => { 368 | reply.cmake.version.string = "3.20.4"; 369 | }); 370 | await expect(loadCMakeApiReplies(cmakeBuildDir)).to.be.rejectedWith( 371 | "Action requires CMake version >= 3.20.5"); 372 | }); 373 | 374 | it("msvc for neither C/C++", async () => { 375 | editReplyContents(cmakeToolchainsReply, (reply) => { 376 | reply.toolchains[CLangIndex].compiler.path = "clang.exe"; 377 | reply.toolchains[CLangIndex].compiler.id = "Clang"; 378 | reply.toolchains[CXXLangIndex].compiler.path = "clang.exe"; 379 | reply.toolchains[CXXLangIndex].compiler.id = "Clang"; 380 | }); 381 | 382 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 383 | expect(() => loadToolchainMap(replyIndexInfo)).to.throw( 384 | "Action requires use of MSVC for either/both C or C++."); 385 | }); 386 | 387 | it("no configuration (multi-config)", async () => { 388 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 389 | expect(() => loadCompileCommands(replyIndexInfo, undefined, [])).to.throw( 390 | "buildConfiguration is required for multi-config CMake Generators."); 391 | }); 392 | 393 | it("invalid configuration (multi-config)", async () => { 394 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 395 | expect(() => loadCompileCommands(replyIndexInfo, "InvalidConfig", [])).to.throw( 396 | "buildConfiguration does not match any available in CMake project."); 397 | }); 398 | 399 | it("invalid configuration (single-config", async () => { 400 | editReplyContents(cmakeCodemodelReply, (reply) => { 401 | reply.configurations.pop(); // Remove second entry, leaving 1 402 | }); 403 | const replyIndexInfo = getApiReplyIndex(cmakeApiDir); 404 | expect(() => loadCompileCommands(replyIndexInfo, "InvalidConfig", [])).to.throw( 405 | "buildConfiguration does not match 'Regular' configuration used by CMake."); 406 | }); 407 | }); 408 | }); -------------------------------------------------------------------------------- /test/sample/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.20.5) 2 | project (Sample) 3 | 4 | add_library (${PROJECT_NAME} 5 | src/fileA.cpp 6 | src/fileb.cpp 7 | ) 8 | 9 | set_target_properties(${PROJECT_NAME} 10 | PROPERTIES 11 | CXX_STANDARD 20 12 | CXX_STANDARD_REQUIRED ON 13 | CXX_EXTENSIONS OFF 14 | ) 15 | 16 | target_include_directories (${PROJECT_NAME} 17 | SYSTEM PUBLIC 18 | ${CMAKE_CURRENT_SOURCE_DIR}/include/system 19 | ) 20 | 21 | target_include_directories (${PROJECT_NAME} 22 | PUBLIC 23 | ${CMAKE_CURRENT_SOURCE_DIR}/include/regular 24 | ) 25 | 26 | 27 | add_subdirectory(failure) -------------------------------------------------------------------------------- /test/sample/Custom.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/sample/Invalid.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/sample/failure/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(TARGET_NAME Failure) 2 | 3 | add_library (${TARGET_NAME} 4 | fail.cpp 5 | ) 6 | 7 | set_target_properties(${TARGET_NAME} 8 | PROPERTIES 9 | CXX_STANDARD 20 10 | CXX_STANDARD_REQUIRED ON 11 | CXX_EXTENSIONS OFF 12 | ) -------------------------------------------------------------------------------- /test/sample/failure/fail.cpp: -------------------------------------------------------------------------------- 1 | // This file is design to purposfully fail compilation because it should be ignored by the 2 | // 'ignoredTargetPaths' action parameter 3 | 4 | invalid C++ -------------------------------------------------------------------------------- /test/sample/include/regular/regular.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Duplicate warnings will be avoided in headers included > 1 times 4 | const char *regularHeaderFunction() 5 | { 6 | std::string s; 7 | return s.c_str(); // C26816 8 | } -------------------------------------------------------------------------------- /test/sample/include/system/system.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Marked as System header in CMake 4 | // No warning should be issued if ignoreSystemHeaders is used 5 | const char *systemHeaderFunction() 6 | { 7 | std::string s; 8 | return s.c_str(); // C26816 9 | } -------------------------------------------------------------------------------- /test/sample/src/fileA.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | std::optional getTempOptional() noexcept { return {}; } 6 | 7 | void C26815() noexcept 8 | { 9 | if (const auto val = *getTempOptional()) // C26815 10 | { 11 | (void)val; 12 | } 13 | } 14 | 15 | int main() noexcept { 16 | return 0; 17 | } -------------------------------------------------------------------------------- /test/sample/src/fileB.cpp: -------------------------------------------------------------------------------- 1 | // Include the same header as fileA to ensure duplicate warnings are not produced 2 | #include 3 | 4 | constexpr void C6001() 5 | { 6 | int x[4]; 7 | x[4] = 1; 8 | } 9 | 10 | int main() noexcept { 11 | return 0; 12 | } 13 | --------------------------------------------------------------------------------