2019-10-30 16:22:33 -07:00
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
|
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
2022-07-21 07:15:26 +07:00
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstdlib>
|
2019-10-30 16:22:33 -07:00
|
|
|
#include <filesystem>
|
2023-08-03 22:16:37 +02:00
|
|
|
#include <format>
|
|
|
|
|
#include <print>
|
2025-06-14 13:23:27 +03:00
|
|
|
#include <ranges>
|
2019-10-30 16:22:33 -07:00
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
2023-08-03 22:16:37 +02:00
|
|
|
#include <type_traits>
|
2019-10-30 16:22:33 -07:00
|
|
|
#include <utility>
|
|
|
|
|
#include <vector>
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
2020-08-21 19:53:55 -07:00
|
|
|
constexpr size_t max_line_length = 120;
|
|
|
|
|
|
2019-10-30 16:22:33 -07:00
|
|
|
class BinaryFile {
|
|
|
|
|
public:
|
2024-08-12 09:41:22 -07:00
|
|
|
explicit BinaryFile(const filesystem::path& filepath) {
|
|
|
|
|
const auto err = _wfopen_s(&m_file, filepath.c_str(), L"rb");
|
|
|
|
|
|
|
|
|
|
if (err != 0 || !m_file) {
|
2023-08-03 22:16:37 +02:00
|
|
|
println(stderr, "Validation failed: {} couldn't be opened.", filepath.string());
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] bool read_next_block(vector<unsigned char>& buffer) {
|
|
|
|
|
constexpr size_t BlockSize = 65536;
|
|
|
|
|
|
|
|
|
|
buffer.resize(BlockSize);
|
|
|
|
|
|
|
|
|
|
const size_t bytes_read = fread(buffer.data(), 1, BlockSize, m_file);
|
|
|
|
|
|
|
|
|
|
buffer.resize(bytes_read);
|
|
|
|
|
|
|
|
|
|
return !buffer.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~BinaryFile() {
|
2024-08-12 09:41:22 -07:00
|
|
|
if (m_file && fclose(m_file) != 0) {
|
2023-08-03 22:16:37 +02:00
|
|
|
println(stderr, "fclose() failed.");
|
2019-10-30 16:22:33 -07:00
|
|
|
abort();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-14 15:03:09 -07:00
|
|
|
BinaryFile(const BinaryFile&) = delete;
|
2019-10-30 16:22:33 -07:00
|
|
|
BinaryFile& operator=(const BinaryFile&) = delete;
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
FILE* m_file{nullptr};
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-14 13:23:27 +03:00
|
|
|
struct line_and_column {
|
|
|
|
|
size_t line = 1;
|
|
|
|
|
size_t column = 1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct character_line_column {
|
|
|
|
|
unsigned char ch = '?';
|
|
|
|
|
line_and_column lc;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
namespace std {
|
|
|
|
|
template <>
|
|
|
|
|
struct formatter<line_and_column> {
|
|
|
|
|
constexpr auto parse(format_parse_context& ctx) {
|
|
|
|
|
return ctx.begin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <class FormatContext>
|
|
|
|
|
auto format(const line_and_column& lc, FormatContext& ctx) const {
|
|
|
|
|
return format_to(ctx.out(), "{}:{}", lc.line, lc.column);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <>
|
|
|
|
|
struct formatter<character_line_column> {
|
|
|
|
|
constexpr auto parse(format_parse_context& ctx) {
|
|
|
|
|
return ctx.begin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <class FormatContext>
|
|
|
|
|
auto format(const character_line_column& clc, FormatContext& ctx) const {
|
|
|
|
|
return format_to(ctx.out(), "0x{:02X} @ {}", static_cast<unsigned int>(clc.ch), clc.lc);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
} // namespace std
|
|
|
|
|
|
2023-08-03 22:16:37 +02:00
|
|
|
template <class... Args>
|
2025-06-14 13:23:27 +03:00
|
|
|
void validation_failure(bool& any_errors, const filesystem::path& filepath, const line_and_column& lc,
|
|
|
|
|
format_string<type_identity_t<Args>...> fmt, Args&&... args) {
|
2022-05-01 03:26:06 -07:00
|
|
|
any_errors = true;
|
2025-06-14 13:23:27 +03:00
|
|
|
print(stderr, "##vso[task.logissue type=error;sourcepath={};linenumber={};columnnumber={}]Validation failed: ",
|
|
|
|
|
filepath.string(), lc.line, lc.column);
|
2023-08-03 22:16:37 +02:00
|
|
|
println(stderr, fmt, forward<Args>(args)...);
|
2022-05-01 03:26:06 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-14 13:23:27 +03:00
|
|
|
template <class... Args>
|
|
|
|
|
void validation_failure(
|
|
|
|
|
bool& any_errors, const filesystem::path& filepath, format_string<type_identity_t<Args>...> fmt, Args&&... args) {
|
|
|
|
|
validation_failure(any_errors, filepath, {1, 1}, fmt, forward<Args>(args)...);
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-30 16:22:33 -07:00
|
|
|
enum class TabPolicy : bool { Forbidden, Allowed };
|
|
|
|
|
|
2022-05-01 03:26:06 -07:00
|
|
|
void scan_file(
|
|
|
|
|
bool& any_errors, const filesystem::path& filepath, const TabPolicy tab_policy, vector<unsigned char>& buffer) {
|
2019-10-30 16:22:33 -07:00
|
|
|
constexpr char CR = '\r';
|
|
|
|
|
constexpr char LF = '\n';
|
|
|
|
|
|
|
|
|
|
bool has_cr = false;
|
|
|
|
|
bool has_lf = false;
|
|
|
|
|
bool has_crlf = false;
|
|
|
|
|
bool has_utf8_bom = false;
|
|
|
|
|
|
2020-08-21 19:53:55 -07:00
|
|
|
size_t overlength_lines = 0;
|
2019-10-30 16:22:33 -07:00
|
|
|
size_t disallowed_characters = 0;
|
|
|
|
|
size_t tab_characters = 0;
|
|
|
|
|
size_t trailing_whitespace_lines = 0;
|
|
|
|
|
|
2025-06-14 13:23:27 +03:00
|
|
|
constexpr size_t max_error_lines_per_file = 8;
|
|
|
|
|
|
|
|
|
|
array<line_and_column, max_error_lines_per_file> overlength_locations{};
|
|
|
|
|
array<line_and_column, max_error_lines_per_file> tab_character_locations{};
|
|
|
|
|
array<line_and_column, max_error_lines_per_file> trailing_whitespace_locations{};
|
|
|
|
|
array<character_line_column, max_error_lines_per_file> disallowed_character_locations{};
|
|
|
|
|
|
2019-10-30 16:22:33 -07:00
|
|
|
unsigned char prev = '@';
|
|
|
|
|
unsigned char previous2 = '@';
|
|
|
|
|
unsigned char previous3 = '@';
|
|
|
|
|
|
2025-06-14 13:23:27 +03:00
|
|
|
size_t lines = 0;
|
2020-08-21 19:53:55 -07:00
|
|
|
size_t columns = 0;
|
|
|
|
|
|
2019-10-30 16:22:33 -07:00
|
|
|
for (BinaryFile binary_file{filepath}; binary_file.read_next_block(buffer);) {
|
|
|
|
|
for (const auto& ch : buffer) {
|
|
|
|
|
if (prev == CR) {
|
|
|
|
|
if (ch == LF) {
|
|
|
|
|
has_crlf = true;
|
|
|
|
|
} else {
|
|
|
|
|
has_cr = true;
|
|
|
|
|
}
|
2025-06-14 13:23:27 +03:00
|
|
|
++lines;
|
2019-10-30 16:22:33 -07:00
|
|
|
} else {
|
|
|
|
|
if (ch == LF) {
|
|
|
|
|
has_lf = true;
|
2025-06-14 13:23:27 +03:00
|
|
|
++lines;
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ch == '\t') {
|
2025-06-14 13:23:27 +03:00
|
|
|
if (tab_characters < max_error_lines_per_file) {
|
|
|
|
|
tab_character_locations[tab_characters] = {lines + 1, columns + 1};
|
|
|
|
|
}
|
2019-10-30 16:22:33 -07:00
|
|
|
++tab_characters;
|
|
|
|
|
} else if (ch == 0xEF || ch == 0xBB || ch == 0xBF) {
|
|
|
|
|
// 0xEF, 0xBB, and 0xBF are the UTF-8 BOM characters.
|
|
|
|
|
// https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
|
|
|
|
|
has_utf8_bom = true;
|
|
|
|
|
} else if (ch != CR && ch != LF && !(ch >= 0x20 && ch <= 0x7E)) {
|
|
|
|
|
// [0x20, 0x7E] are the printable characters, including the space character.
|
|
|
|
|
// https://en.wikipedia.org/wiki/ASCII#Printable_characters
|
2025-06-14 13:23:27 +03:00
|
|
|
if (disallowed_characters < max_error_lines_per_file) {
|
|
|
|
|
disallowed_character_locations[disallowed_characters] = {ch, {lines + 1, columns + 1}};
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
2025-06-14 13:23:27 +03:00
|
|
|
++disallowed_characters;
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
|
2020-08-21 19:53:55 -07:00
|
|
|
if (ch == CR || ch == LF) {
|
|
|
|
|
if (prev == ' ' || prev == '\t') {
|
2025-06-14 13:23:27 +03:00
|
|
|
if (trailing_whitespace_lines < max_error_lines_per_file) {
|
|
|
|
|
trailing_whitespace_locations[trailing_whitespace_lines] = {lines + 1, columns + 1};
|
|
|
|
|
}
|
2020-08-21 19:53:55 -07:00
|
|
|
++trailing_whitespace_lines;
|
|
|
|
|
}
|
2019-10-30 16:22:33 -07:00
|
|
|
|
2020-08-21 19:53:55 -07:00
|
|
|
if (columns > max_line_length) {
|
2025-06-14 13:23:27 +03:00
|
|
|
if (overlength_lines < max_error_lines_per_file) {
|
|
|
|
|
overlength_locations[overlength_lines] = {lines + 1, columns + 1};
|
|
|
|
|
}
|
2020-08-21 19:53:55 -07:00
|
|
|
++overlength_lines;
|
|
|
|
|
}
|
|
|
|
|
columns = 0;
|
|
|
|
|
} else {
|
|
|
|
|
++columns;
|
|
|
|
|
}
|
2019-10-30 16:22:33 -07:00
|
|
|
previous3 = exchange(previous2, exchange(prev, ch));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (prev == CR) { // file ends with CR
|
|
|
|
|
has_cr = true;
|
2025-06-14 13:23:27 +03:00
|
|
|
++lines;
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (has_cr) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file contains CR line endings (possibly damaged CRLF).");
|
2019-10-30 16:22:33 -07:00
|
|
|
} else if (has_lf && has_crlf) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file contains mixed line endings (both LF and CRLF).");
|
2019-10-30 16:22:33 -07:00
|
|
|
} else if (has_lf) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file contains LF line endings.");
|
2019-10-30 16:22:33 -07:00
|
|
|
|
|
|
|
|
if (prev != LF) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file doesn't end with a newline.");
|
2019-10-30 16:22:33 -07:00
|
|
|
} else if (previous2 == LF) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file ends with multiple newlines.");
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
} else if (has_crlf) {
|
|
|
|
|
if (previous2 != CR || prev != LF) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file doesn't end with a newline.");
|
2019-10-30 16:22:33 -07:00
|
|
|
} else if (previous3 == LF) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file ends with multiple newlines.");
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
} else {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file doesn't contain any newlines.");
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (has_utf8_bom) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file contains UTF-8 BOM characters.");
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tab_policy == TabPolicy::Forbidden && tab_characters != 0) {
|
2025-06-14 13:23:27 +03:00
|
|
|
validation_failure(any_errors, filepath, tab_character_locations[0],
|
|
|
|
|
"file contains {} tab characters. Lines and columns (up to {}): {}.", tab_characters,
|
|
|
|
|
max_error_lines_per_file, tab_character_locations | views::take(tab_characters));
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trailing_whitespace_lines != 0) {
|
2025-06-14 13:23:27 +03:00
|
|
|
validation_failure(any_errors, filepath, trailing_whitespace_locations[0],
|
|
|
|
|
"file contains {} lines with trailing whitespace. Lines and columns (up to {}): {}.",
|
|
|
|
|
trailing_whitespace_lines, max_error_lines_per_file,
|
|
|
|
|
trailing_whitespace_locations | views::take(trailing_whitespace_lines));
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
2020-08-21 19:53:55 -07:00
|
|
|
|
|
|
|
|
if (overlength_lines != 0) {
|
|
|
|
|
static constexpr array checked_extensions{
|
|
|
|
|
// line length should be capped in files with these extensions:
|
|
|
|
|
L""sv,
|
2020-08-26 01:18:26 -07:00
|
|
|
L".cmd"sv,
|
2020-08-21 19:53:55 -07:00
|
|
|
L".cpp"sv,
|
|
|
|
|
L".h"sv,
|
|
|
|
|
L".hpp"sv,
|
2020-08-26 01:18:26 -07:00
|
|
|
L".md"sv,
|
|
|
|
|
L".ps1"sv,
|
|
|
|
|
L".py"sv,
|
2020-08-21 19:53:55 -07:00
|
|
|
L".yml"sv,
|
|
|
|
|
};
|
2022-09-13 17:35:02 -04:00
|
|
|
static_assert(ranges::is_sorted(checked_extensions));
|
2020-08-21 19:53:55 -07:00
|
|
|
|
2022-09-13 17:35:02 -04:00
|
|
|
if (ranges::binary_search(checked_extensions, filepath.extension().wstring())) {
|
2025-06-14 13:23:27 +03:00
|
|
|
validation_failure(any_errors, filepath, overlength_locations[0],
|
|
|
|
|
"file contains {} lines with more than {} columns. Lines and columns (up to {}): {}.", overlength_lines,
|
|
|
|
|
max_line_length, max_error_lines_per_file, overlength_locations | views::take(overlength_lines));
|
2020-08-21 19:53:55 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-14 13:23:27 +03:00
|
|
|
|
|
|
|
|
if (disallowed_characters != 0) {
|
|
|
|
|
validation_failure(any_errors, filepath, disallowed_character_locations[0].lc,
|
|
|
|
|
"file contains {} disallowed characters. Locations (up to {}): {}.", disallowed_characters,
|
|
|
|
|
max_error_lines_per_file, disallowed_character_locations | views::take(disallowed_characters));
|
|
|
|
|
}
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
static constexpr array skipped_directories{
|
2020-04-09 22:54:59 -07:00
|
|
|
L".git"sv,
|
|
|
|
|
L".vs"sv,
|
|
|
|
|
L".vscode"sv,
|
|
|
|
|
L"__pycache__"sv,
|
2021-08-27 06:38:38 +02:00
|
|
|
L"boost-math"sv,
|
2020-04-30 02:49:14 -07:00
|
|
|
L"build"sv,
|
2022-06-30 19:14:34 -07:00
|
|
|
L"google-benchmark"sv,
|
2020-04-09 22:54:59 -07:00
|
|
|
L"llvm-project"sv,
|
|
|
|
|
L"out"sv,
|
2019-10-30 16:22:33 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static constexpr array skipped_extensions{
|
2020-04-09 22:54:59 -07:00
|
|
|
L".dll"sv,
|
|
|
|
|
L".exe"sv,
|
|
|
|
|
L".obj"sv,
|
2019-10-30 16:22:33 -07:00
|
|
|
};
|
|
|
|
|
|
2024-05-20 17:17:37 -07:00
|
|
|
// CODE_OF_CONDUCT.md and SECURITY.md are copied exactly from https://github.com/microsoft/repo-templates
|
|
|
|
|
static constexpr array skipped_relative_paths{
|
|
|
|
|
LR"(.\CODE_OF_CONDUCT.md)"sv,
|
|
|
|
|
LR"(.\SECURITY.md)"sv,
|
|
|
|
|
};
|
|
|
|
|
|
2022-05-01 03:26:06 -07:00
|
|
|
// make sure someone doesn't accidentally include a diff in the tree
|
|
|
|
|
static constexpr array bad_extensions{
|
|
|
|
|
L".diff"sv,
|
|
|
|
|
L".patch"sv,
|
|
|
|
|
};
|
|
|
|
|
|
2019-10-30 16:22:33 -07:00
|
|
|
static constexpr array tabby_filenames{
|
2020-04-09 22:54:59 -07:00
|
|
|
L".gitmodules"sv,
|
2019-10-30 16:22:33 -07:00
|
|
|
};
|
|
|
|
|
|
2023-09-29 09:45:22 -07:00
|
|
|
static constexpr array tabby_extensions{
|
|
|
|
|
L".lst"sv,
|
|
|
|
|
};
|
|
|
|
|
|
2022-09-13 17:35:02 -04:00
|
|
|
static_assert(ranges::is_sorted(skipped_directories));
|
|
|
|
|
static_assert(ranges::is_sorted(skipped_extensions));
|
2024-05-20 17:17:37 -07:00
|
|
|
static_assert(ranges::is_sorted(skipped_relative_paths));
|
2022-09-13 17:35:02 -04:00
|
|
|
static_assert(ranges::is_sorted(bad_extensions));
|
|
|
|
|
static_assert(ranges::is_sorted(tabby_filenames));
|
2023-09-29 09:45:22 -07:00
|
|
|
static_assert(ranges::is_sorted(tabby_extensions));
|
2019-10-30 16:22:33 -07:00
|
|
|
|
|
|
|
|
vector<unsigned char> buffer; // reused for performance
|
2022-05-01 03:26:06 -07:00
|
|
|
bool any_errors = false;
|
2019-10-30 16:22:33 -07:00
|
|
|
|
|
|
|
|
for (filesystem::recursive_directory_iterator rdi{"."}, last; rdi != last; ++rdi) {
|
2020-04-09 22:54:59 -07:00
|
|
|
const filesystem::path& filepath = rdi->path();
|
|
|
|
|
|
|
|
|
|
const wstring filename = filepath.filename().wstring();
|
2019-10-30 16:22:33 -07:00
|
|
|
|
|
|
|
|
if (!rdi->is_regular_file()) {
|
|
|
|
|
if (rdi->is_directory()) {
|
2022-09-13 17:35:02 -04:00
|
|
|
if (ranges::binary_search(skipped_directories, filename)) {
|
2019-10-30 16:22:33 -07:00
|
|
|
rdi.disable_recursion_pending();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 22:54:59 -07:00
|
|
|
const wstring& relative_path = filepath.native();
|
|
|
|
|
|
2024-05-20 17:17:37 -07:00
|
|
|
if (ranges::binary_search(skipped_relative_paths, relative_path)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 22:54:59 -07:00
|
|
|
constexpr size_t maximum_relative_path_length = 120;
|
|
|
|
|
if (relative_path.size() > maximum_relative_path_length) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "filepath is too long ({} characters; the limit is {}).",
|
2022-05-01 03:26:06 -07:00
|
|
|
relative_path.size(), maximum_relative_path_length);
|
2020-04-09 22:54:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (relative_path.find(L' ') != wstring::npos) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "filepath contains spaces.");
|
2020-04-09 22:54:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wstring extension = filepath.extension().wstring();
|
2019-10-30 16:22:33 -07:00
|
|
|
|
2022-09-13 17:35:02 -04:00
|
|
|
if (ranges::binary_search(skipped_extensions, extension)) {
|
2019-10-30 16:22:33 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-13 17:35:02 -04:00
|
|
|
if (ranges::binary_search(bad_extensions, extension)) {
|
2023-08-03 22:16:37 +02:00
|
|
|
validation_failure(any_errors, filepath, "file should not be checked in.");
|
2022-05-01 03:26:06 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-29 09:45:22 -07:00
|
|
|
const TabPolicy tab_policy{
|
|
|
|
|
ranges::binary_search(tabby_filenames, filename) || ranges::binary_search(tabby_extensions, extension)};
|
2019-10-30 16:22:33 -07:00
|
|
|
|
2022-05-01 03:26:06 -07:00
|
|
|
scan_file(any_errors, filepath, tab_policy, buffer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (any_errors) {
|
2023-08-03 22:16:37 +02:00
|
|
|
println(
|
|
|
|
|
stderr, "##vso[task.logissue type=warning]If your build fails here, you need to fix the listed issues.");
|
|
|
|
|
println(stderr, "##vso[task.complete result=Failed]DONE");
|
2019-10-30 16:22:33 -07:00
|
|
|
}
|
|
|
|
|
}
|