├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .swiftlint.yml
├── CODEOWNERS
├── Dockerfile
├── LICENSE
├── NOTICE
├── Package.resolved
├── Package.swift
├── Package@swift-4.2.swift
├── Package@swift-5.0.swift
├── README.md
├── Rakefile
├── Resources
├── css
│ └── styles.css
├── index.html
├── js
│ ├── app.js
│ ├── build.js
│ └── step.js
└── step.html
├── Sources
├── XCLogParser
│ ├── XCLogParserError.swift
│ ├── activityparser
│ │ ├── ActivityParser.swift
│ │ └── IDEActivityModel.swift
│ ├── commands
│ │ ├── Action.swift
│ │ ├── ActionOptions.swift
│ │ ├── Command.swift
│ │ ├── CommandHandler.swift
│ │ ├── LogOptions.swift
│ │ └── Version.swift
│ ├── extensions
│ │ ├── ArrayExtension.swift
│ │ ├── EncodableExtension.swift
│ │ ├── NSRegularExpressionExtension.swift
│ │ └── URLExtension.swift
│ ├── generated
│ │ └── HtmlReporterResources.swift
│ ├── lexer
│ │ ├── LexRedactor.swift
│ │ ├── Lexer.swift
│ │ ├── LexerModel.swift
│ │ ├── LogRedactor.swift
│ │ └── String+BuildSpecificInformationRemoval.swift
│ ├── loglocation
│ │ ├── LogError.swift
│ │ ├── LogFinder.swift
│ │ └── LogLoader.swift
│ ├── logmanifest
│ │ ├── LogManifest.swift
│ │ └── LogManifestModel.swift
│ ├── output
│ │ ├── FileOutput.swift
│ │ ├── ReporterOutput.swift
│ │ └── StandardOutput.swift
│ ├── parser
│ │ ├── BuildStatusSanitizer.swift
│ │ ├── BuildStep+Builder.swift
│ │ ├── BuildStep+Parser.swift
│ │ ├── BuildStep.swift
│ │ ├── ClangCompilerParser.swift
│ │ ├── Contains.swift
│ │ ├── IDEActivityLogSection+Builders.swift
│ │ ├── IDEActivityLogSection+Parsing.swift
│ │ ├── LinkerStatistics.swift
│ │ ├── MachineNameReader.swift
│ │ ├── Notice+Parser.swift
│ │ ├── Notice.swift
│ │ ├── NoticeType.swift
│ │ ├── ParserBuildSteps.swift
│ │ ├── Prefix.swift
│ │ ├── StringExtension.swift
│ │ ├── Suffix.swift
│ │ ├── SwiftCompilerFunctionTimeOptionParser.swift
│ │ ├── SwiftCompilerParser.swift
│ │ ├── SwiftCompilerTimeOptionParser.swift
│ │ ├── SwiftCompilerTypeCheckOptionParser.swift
│ │ ├── SwiftFunctionTime.swift
│ │ └── SwiftTypeCheck.swift
│ └── reporter
│ │ ├── ChromeTracerReporter.swift
│ │ ├── FlatJsonReporter.swift
│ │ ├── HtmlReporter.swift
│ │ ├── IssuesReporter.swift
│ │ ├── JsonReporter.swift
│ │ ├── LogReporter.swift
│ │ ├── Reporter.swift
│ │ └── SummaryJsonReporter.swift
├── XCLogParserApp
│ ├── commands
│ │ ├── DumpCommand.swift
│ │ ├── MainCommand.swift
│ │ ├── ManifestCommand.swift
│ │ ├── Optional+Blank.swift
│ │ ├── ParseCommand.swift
│ │ ├── String+Blank.swift
│ │ └── VersionCommand.swift
│ └── main.swift
└── XcodeHasher
│ └── XcodeHasher.swift
├── Tests
├── LinuxMain.swift
└── XCLogParserTests
│ ├── ActivityParserTests.swift
│ ├── BuildStatusSanitizerTests.swift
│ ├── BuildStep+TestUtils.swift
│ ├── ChromeTracerOutputTests.swift
│ ├── ClangCompilerParserTests.swift
│ ├── IssuesReporterTests.swift
│ ├── LexRedactorTests.swift
│ ├── LexerTests.swift
│ ├── LogFinderTests.swift
│ ├── LogManifestTests.swift
│ ├── ParserTests.swift
│ ├── ReporterTests.swift
│ ├── String+BuildSpecificInformationRemovalTests.swift
│ ├── SwiftCompilerParserTests.swift
│ ├── TestUtils.swift
│ └── XCTestManifests.swift
├── build_release_in_docker.sh
├── ci
├── ci.sh
└── helpers.sh
├── docs
├── JSON Format.md
└── Xcactivitylog Format.md
├── images
├── kickstarter-ios-chrome-tracer.png
├── kickstarter-ios.png
└── post-action-run-script.png
└── run_in_docker.sh
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | SwiftLint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - name: SwiftLint
11 | uses: norio-nomura/action-swiftlint@3.1.0
12 | with:
13 | args: --strict
14 |
15 | macOS:
16 | runs-on: macos-13
17 | env:
18 | XCODE_VERSION: ${{ '14.1' }}
19 | steps:
20 | - name: Select Xcode
21 | run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
22 | - name: Checkout
23 | uses: actions/checkout@v4
24 | - name: Build and Run
25 | run: rake build[release]
26 | - name: Test
27 | run: rake test
28 |
29 | linux:
30 | runs-on: ubuntu-latest
31 | container:
32 | image: swift:5.7.1
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 | - name: Install Ruby
37 | run: apt-get update && apt-get install -y ruby zlib1g-dev
38 | - name: Build and Run
39 | run: LANG=en_US.UTF-8 LC_CTYPE=UTF-8 rake build[release]
40 | - name: Test
41 | run: LANG=en_US.UTF-8 LC_CTYPE=UTF-8 rake test
42 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release_binaries
2 | on:
3 | release:
4 | types: created
5 |
6 | jobs:
7 | macOS:
8 | name: Add macOS binaries to release
9 | runs-on: macos-13
10 | env:
11 | XCODE_VERSION: ${{ '14.1' }}
12 | steps:
13 | - name: Select Xcode
14 | run: "sudo xcode-select -s /Applications/Xcode_$XCODE_VERSION.app"
15 | - name: Checkout
16 | uses: actions/checkout@v1
17 | - name: Set tag name
18 | run: echo "TAG_NAME=$(echo $GITHUB_REF | cut -c 11-)" >> $GITHUB_ENV
19 | - name: Build x86_64-apple-macosx
20 | run: rake 'build[release, x86_64-apple-macosx]'
21 | - name: Zip x86_64-apple-macosx release
22 | run: "mkdir releases && zip -j releases/XCLogParser-macOS-x86_64-$TAG_NAME.zip .build/release/xclogparser"
23 | - name: Save x86_64 executable to be lipo'd later
24 | run: mkdir tmp && cp .build/release/xclogparser tmp/xclogparser-x86_64
25 | - name: Zip x86_64-apple-macosx release
26 | run: "zip -j releases/XCLogParser-macOS-x86_64-$TAG_NAME.zip .build/release/xclogparser"
27 | - name: Build arm64-apple-macosx
28 | run: rake 'build[release, arm64-apple-macosx]'
29 | - name: Zip arm64-apple-macosx release
30 | run: "zip -j releases/XCLogParser-macOS-arm64-$TAG_NAME.zip .build/release/xclogparser"
31 | - name: Lipo macOS executables
32 | run: "lipo -create -output tmp/xclogparser tmp/xclogparser-x86_64 .build/release/xclogparser"
33 | - name: Zip x86_64-arm64-apple-macosx release
34 | run: "zip -j releases/XCLogParser-macOS-x86-64-arm64-$TAG_NAME.zip tmp/xclogparser"
35 | - name: Upload binaries to release
36 | uses: svenstaro/upload-release-action@v1-release
37 | with:
38 | repo_token: ${{ secrets.GITHUB_TOKEN }}
39 | file: releases/*
40 | file_glob: true
41 | tag: ${{ github.ref }}
42 | overwrite: true
43 |
44 | linux:
45 | name: Add Linux binaries to release
46 | runs-on: ubuntu-latest
47 | container:
48 | image: swift:5.7.1
49 | steps:
50 | - name: Checkout
51 | uses: actions/checkout@v1
52 | - name: Install Ruby
53 | run: apt-get update && apt-get install -y ruby zlib1g-dev
54 | - name: Build
55 | run: LANG=en_US.UTF-8 LC_CTYPE=UTF-8 rake build[release]
56 | - name: Set tag name
57 | run: echo "TAG_NAME=$(echo $GITHUB_REF | cut -c 11-)" >> $GITHUB_ENV
58 | - name: Zip release
59 | uses: montudor/action-zip@v0.1.0
60 | with:
61 | args: zip -j XCLogParser-linux-amd64.zip .build/release/xclogparser
62 | - name: Rename zip
63 | run: "mkdir releases && mv XCLogParser-linux-amd64.zip releases/XCLogParser-linux-amd64-$TAG_NAME.zip"
64 | - name: Upload binaries to release
65 | uses: svenstaro/upload-release-action@v1-release
66 | with:
67 | repo_token: ${{ secrets.GITHUB_TOKEN }}
68 | file: releases/*
69 | file_glob: true
70 | tag: ${{ github.ref }}
71 | overwrite: true
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 |
6 | ## Xcode Build generated
7 | build/
8 | DerivedData/
9 |
10 | /samples
11 | /releases
12 |
13 | .vscode
14 |
15 | # Xcode 11
16 | .swiftpm/
17 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | included:
2 | - Sources
3 | - Tests
4 |
5 | excluded:
6 | - Sources/XCLogParser/generated
7 | - Tests/XCLogParserTests/XCTestManifests.swift
8 |
9 | disabled_rules:
10 | - trailing_comma
11 |
12 | type_name:
13 | min_length: 4
14 | max_length: 60
15 |
16 | identifier_name:
17 | min_length: 3
18 | max_length: 60
19 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @xclogparser-committers @polac24 @ecamacho @aleksandergrzyb @CognitiveDisson
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM swift:5.5
2 | RUN apt-get update && apt-get install -y zlib1g-dev ruby
3 | CMD cd xclogparser && swift build
4 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | XcLogParser
2 | Copyright 2019 Spotify AB
3 |
4 | This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/):
5 | https://github.com/krzyzanowskim/CryptoSwift
6 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CryptoSwift",
6 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "e2bc81be54d71d566a52ca17c3983d141c30aa70",
10 | "version": "1.3.3"
11 | }
12 | },
13 | {
14 | "package": "Gzip",
15 | "repositoryURL": "https://github.com/1024jp/GzipSwift",
16 | "state": {
17 | "branch": null,
18 | "revision": "ba0b6cb51cc6202f896e469b87d2889a46b10d1b",
19 | "version": "5.1.1"
20 | }
21 | },
22 | {
23 | "package": "PathKit",
24 | "repositoryURL": "https://github.com/kylef/PathKit.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
28 | "version": "1.0.1"
29 | }
30 | },
31 | {
32 | "package": "Spectre",
33 | "repositoryURL": "https://github.com/kylef/Spectre.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7",
37 | "version": "0.10.1"
38 | }
39 | },
40 | {
41 | "package": "swift-argument-parser",
42 | "repositoryURL": "https://github.com/apple/swift-argument-parser",
43 | "state": {
44 | "branch": null,
45 | "revision": "fddd1c00396eed152c45a46bea9f47b98e59301d",
46 | "version": "1.2.0"
47 | }
48 | }
49 | ]
50 | },
51 | "version": 1
52 | }
53 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "XCLogParser",
8 | platforms: [.macOS(.v10_13)],
9 | products: [
10 | .executable(name: "xclogparser", targets: ["XCLogParserApp"]),
11 | .library(name: "XCLogParser", targets: ["XCLogParser"])
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/1024jp/GzipSwift", from: "5.1.0"),
15 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .exact("1.3.3")),
16 | .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.1"),
17 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
18 | ],
19 | targets: [
20 | .target(
21 | name:"XcodeHasher",
22 | dependencies: ["CryptoSwift"]
23 | ),
24 | .target(
25 | name: "XCLogParser",
26 | dependencies: [
27 | .product(name: "Gzip", package: "GzipSwift"),
28 | "XcodeHasher",
29 | "PathKit"
30 | ]
31 | ),
32 | .executableTarget(
33 | name: "XCLogParserApp",
34 | dependencies: [
35 | "XCLogParser",
36 | .product(name: "ArgumentParser", package: "swift-argument-parser")
37 | ]
38 | ),
39 | .testTarget(
40 | name: "XCLogParserTests",
41 | dependencies: ["XCLogParser"]
42 | ),
43 | ]
44 |
45 | )
46 |
--------------------------------------------------------------------------------
/Package@swift-4.2.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.2
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "XCLogParser",
8 | products: [
9 | .executable(name: "xclogparser", targets: ["XCLogParserApp"]),
10 | .library(name: "XCLogParser", targets: ["XCLogParser"])
11 | ],
12 | dependencies: [
13 | .package(url: "https://github.com/1024jp/GzipSwift", from: "4.1.0"),
14 | .package(url: "https://github.com/Carthage/Commandant.git", from: "0.16.0"),
15 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "0.15.0"),
16 | .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.0"),
17 | .package(url: "https://github.com/antitypical/Result.git", from: "4.0.0"),
18 | ],
19 | targets: [
20 | .target(
21 | name:"XcodeHasher",
22 | dependencies: ["CryptoSwift"]
23 | ),
24 | .target(
25 | name: "XCLogParser",
26 | dependencies: ["Gzip", "XcodeHasher", "PathKit"]
27 | ),
28 | .target(
29 | name: "XCLogParserApp",
30 | dependencies: ["XCLogParser", "Commandant", "Result"]
31 | ),
32 | .testTarget(
33 | name: "XCLogParserTests",
34 | dependencies: ["XCLogParser"]
35 | ),
36 | ]
37 |
38 | )
39 |
--------------------------------------------------------------------------------
/Package@swift-5.0.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "XCLogParser",
8 | platforms: [.macOS(.v10_13)],
9 | products: [
10 | .executable(name: "xclogparser", targets: ["XCLogParserApp"]),
11 | .library(name: "XCLogParser", targets: ["XCLogParser"])
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/1024jp/GzipSwift", from: "5.1.0"),
15 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .exact("1.3.3")),
16 | .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.1"),
17 | .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")),
18 | ],
19 | targets: [
20 | .target(
21 | name:"XcodeHasher",
22 | dependencies: ["CryptoSwift"]
23 | ),
24 | .target(
25 | name: "XCLogParser",
26 | dependencies: ["Gzip", "XcodeHasher", "PathKit"]
27 | ),
28 | .target(
29 | name: "XCLogParserApp",
30 | dependencies: [
31 | "XCLogParser",
32 | .product(name: "ArgumentParser", package: "swift-argument-parser")
33 | ]
34 | ),
35 | .testTarget(
36 | name: "XCLogParserTests",
37 | dependencies: ["XCLogParser"]
38 | ),
39 | ]
40 |
41 | )
42 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | ################################
4 | # Rake configuration
5 | ################################
6 |
7 | # Paths
8 | BUILD_DIR = File.join('.build').freeze
9 | RELEASES_ROOT_DIR = File.join('releases').freeze
10 | EXECUTABLE_NAME = 'xclogparser'.freeze
11 | PROJECT_NAME = 'XCLogParser'.freeze
12 | INSTALLATION_PREFIX = '/usr/local'.freeze
13 |
14 |
15 | desc 'Build XCLogParser'
16 | task :build, [:configuration, :arch, :sdks, :is_archive] => ['gen_resources'] do |task, args|
17 | # Set task defaults
18 | args.with_defaults(:configuration => 'debug', :sdks => ['macos'])
19 |
20 | unless args.configuration == 'Debug'.downcase || args.configuration == 'Release'.downcase
21 | fail("Unsupported configuration. Valid values: ['debug', 'release']. Found '#{args.configuration}''")
22 | end
23 |
24 | # Clean data generated by spm
25 | system("rm -rf #{BUILD_DIR} > /dev/null 2>&1")
26 |
27 | # Build
28 | build_paths = []
29 | args.sdks.each do |sdk|
30 | spm_build(args.configuration, args.arch)
31 | # path of the executable looks like: `.build/(debug|release)/xclogparser`
32 | build_path = File.join(BUILD_DIR, args.configuration, EXECUTABLE_NAME)
33 | build_paths.push(build_path)
34 | end
35 |
36 | puts(build_paths)
37 |
38 | if args.configuration == 'Release'.downcase and args.is_archive
39 | puts "Creating release zip"
40 | create_release_zip(build_paths)
41 | end
42 | end
43 |
44 | desc 'Run tests with SPM'
45 | task :test => ['gen_resources'] do
46 | spm_test
47 | end
48 |
49 | desc 'Build and install XCLogParser'
50 | task :install, [:prefix] do |t, args|
51 | Rake::Task["build"].invoke('release')
52 | # install binary in given prefix or fallback to default one
53 | installation_prefix = args[:prefix].nil? || args[:prefix].nil? ? INSTALLATION_PREFIX : args[:prefix]
54 | bin_dir = installation_bin_dir(installation_prefix)
55 | system("mkdir -p #{bin_dir}")
56 | system("cp -f .build/release/#{EXECUTABLE_NAME} #{bin_dir}")
57 | end
58 |
59 | desc 'Create a release zip'
60 | task :archive do
61 | Rake::Task["build"].invoke('release', ['macos'], 'true')
62 | end
63 |
64 | desc 'Generates a Swift class with the file content from Resources'
65 | task :gen_resources do
66 | gen_fake_resources
67 | end
68 |
69 | ################################
70 | # Helper functions
71 | ################################
72 |
73 | def spm_build(configuration, arch)
74 | spm_cmd = "swift build "\
75 | "-c #{configuration} "\
76 | "#{arch.nil? ? "" : "--triple #{arch}"} "\
77 | "--disable-sandbox"
78 | p spm_cmd
79 | system(spm_cmd) or abort "Build failure"
80 | end
81 |
82 | def spm_test()
83 | spm_cmd = "swift test"
84 | system(spm_cmd) or abort "Test failure"
85 | end
86 |
87 | def installation_bin_dir(dir)
88 | "#{dir}/bin"
89 | end
90 |
91 | def create_release_zip(build_paths)
92 | release_dir = RELEASES_ROOT_DIR
93 |
94 | # Create and move files into the release directory
95 | mkdir_p release_dir
96 | cp_r build_paths[0], release_dir
97 |
98 | # Get the current version from the Swift Version file
99 | version = get_version
100 | unless version
101 | fail("Version not found")
102 | end
103 |
104 | output_artifact_basename = "#{PROJECT_NAME}-#{version}.zip"
105 |
106 | Dir.chdir(release_dir) do
107 | # -X: no extras (uid, gid, file times, ...)
108 | # -r: recursive
109 | system("zip -X -r #{output_artifact_basename} .") or abort "zip failure"
110 | # List contents of zip file
111 | system("unzip -l #{output_artifact_basename}") or abort "unzip failure"
112 | end
113 | end
114 |
115 | def get_version
116 | version_file = File.open('Sources/XCLogParser/commands/Version.swift').read
117 | /let current = \"(?.*)\"/ =~ version_file
118 | version
119 | end
120 |
121 | def gen_fake_resources
122 | swift_dir = 'Sources/XCLogParser/generated'
123 |
124 | css = File.open('Resources/css/styles.css', 'r:UTF-8', &:read)
125 | app_js = File.open('Resources/js/app.js', 'r:UTF-8', &:read)
126 | build_js = File.open('Resources/js/build.js', 'r:UTF-8', &:read)
127 | index_html = File.open('Resources/index.html', 'r:UTF-8', &:read)
128 | step_html = File.open('Resources/step.html', 'r:UTF-8', &:read)
129 | step_js = File.open('Resources/js/step.js', 'r:UTF-8', &:read)
130 | swift_content = <<-eos
131 | import Foundation
132 |
133 | /// File generated by the rake `gen_resources` command.
134 | /// Do not edit
135 | public struct HtmlReporterResources {
136 |
137 | public static let css =
138 | """
139 | #{css}
140 | """
141 |
142 | public static let appJS =
143 | """
144 | #{app_js}
145 | """
146 |
147 | public static let buildJS =
148 | """
149 | #{build_js}
150 | """
151 |
152 | public static let indexHTML =
153 | """
154 | #{index_html}
155 | """
156 |
157 | public static let stepHTML =
158 | """
159 | #{step_html}
160 | """
161 |
162 | public static let stepJS =
163 | """
164 | #{step_js}
165 | """
166 |
167 | }
168 | eos
169 | Dir.mkdir swift_dir unless File.exist?(swift_dir)
170 | File.open("#{swift_dir}/HtmlReporterResources.swift", 'w') { |f|
171 | f.write(swift_content)
172 | }
173 | end
174 |
--------------------------------------------------------------------------------
/Resources/css/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #f5f5f5;
3 | }
4 |
5 | div.info-box {
6 | height: 120px;
7 | }
8 |
9 | main.main {
10 | padding-top: 30px;
11 | }
12 |
13 | .swift {
14 | background-color: #EE5541;
15 | }
16 |
17 | .objc {
18 | background-color: #5E565A;
19 | }
20 |
21 | .xc-build-info {
22 | text-align: right;
23 | color: #6c757d;
24 | }
25 | .xc-content {
26 | margin: 10px;
27 | }
28 |
29 | .xc-navbar {
30 | border: 1px solid rgba(0,0,0,.125);
31 | background-color: rgba(0,0,0,.03);
32 | border-bottom: 1px solid rgba(0,0,0,.125);
33 | }
34 |
35 | .navbar-light .navbar-brand {
36 | color: gray;
37 | }
38 |
39 | .xc-header {
40 | height: 40px;
41 | }
42 |
43 | .xc-topboxes {
44 | margin-left: 35px;
45 | margin-right: 35px;
46 | margin: 10px;
47 | }
48 |
49 | .callout-danger {
50 | border-left: 4px solid red !important;
51 | }
52 |
53 | .callout-warning {
54 | border-left: 4px solid #ffc107 !important;
55 | }
56 |
57 | .callout {
58 | position: relative;
59 | padding: 0 1rem;
60 | margin: 1rem 0;
61 | border-left: 4px solid #c8ced3;
62 | border-radius: .25rem;
63 | }
64 |
65 | .header-title {
66 | float: left
67 | }
68 | .header-action {
69 | float: right
70 | }
71 |
72 | .header-action a {
73 | color: gray;
74 | text-decoration: none;
75 | }
76 |
77 | .xc-card-body {
78 | padding: .75em;
79 | }
80 |
--------------------------------------------------------------------------------
/Resources/js/build.js:
--------------------------------------------------------------------------------
1 | const buildData = {{build}};
--------------------------------------------------------------------------------
/Resources/js/step.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | const incidentSource = "" +
21 | "{{#each details}}" +
22 | "{{#if documentURL}}" +
23 | "- {{clangWarning}} {{title}} " +
24 | "In {{documentURL}} Line {{startingLineNumber}} column {{startingColumnNumber}}
" +
25 | "{{else}}" +
26 | "- {{clangWarning}} {{title}}
" +
27 | "{{/if}}" +
28 | "{{/each}}" +
29 | "
" +
30 | "
" +
31 | "";
32 |
33 | const incidentTemplate = Handlebars.compile(incidentSource);
34 |
35 | const swiftFunctionSource = "" +
36 | "" +
37 | "" +
38 | "Duration (ms) | " +
39 | "Function | " +
40 | "Line | " +
41 | "Column | " +
42 | "Occurrences | " +
43 | "Cumulative (ms) | " +
44 | "
" +
45 | "" +
46 | "{{#each functions}}" +
47 | "" +
48 | "{{durationMS}} | " +
49 | "{{signature}} | " +
50 | "{{startingLine}} | " +
51 | "{{startingColumn}} | " +
52 | "{{occurrences}} | " +
53 | "{{cumulative}} | " +
54 | "
" +
55 | "{{/each}}" +
56 | "
";
57 |
58 | const swiftTypeCheckSource = "" +
59 | "" +
60 | "" +
61 | "Duration (ms) | " +
62 | "Line | " +
63 | "Column | " +
64 | "Occurrences | " +
65 | "Cumulative (ms) | " +
66 | "
" +
67 | "" +
68 | "{{#each functions}}" +
69 | "" +
70 | "{{durationMS}} | " +
71 | "{{startingLine}} | " +
72 | "{{startingColumn}} | " +
73 | "{{occurrences}} | " +
74 | "{{cumulative}} | " +
75 | "
" +
76 | "{{/each}}" +
77 | "
";
78 |
79 | const swiftFunctionWarning = "" +
80 | "Warning: No Swift function compilation times were found." +
81 | "
" +
82 | "Did you compile your project with the flags -Xfrontend -debug-time-function-bodies?" +
83 | "
";
84 |
85 | const swiftTypeCheckWarning = "" +
86 | "Warning: No Swiftc type checks times were found." +
87 | "
" +
88 | "Did you compile your project with the flags -Xfrontend -debug-time-expression-type-checking?" +
89 | "
";
90 |
91 | const swiftFunctionTemplate = Handlebars.compile(swiftFunctionSource);
92 |
93 | const swiftTypeCheckTemplate = Handlebars.compile(swiftTypeCheckSource);
94 |
95 | const timestampFormat = 'MMMM Do YYYY, h:mm:ss a';
96 |
97 | showStep();
98 |
99 | $(function () {
100 | $('[data-toggle="tooltip"]').tooltip()
101 | });
102 |
103 | function showStep() {
104 | const step = loadStep();
105 | if (step != null) {
106 | $('#info-title').html(step.title);
107 | $('#info-cache').html(step.fetchedFromCache);
108 | $('#info-signature').html(step.signature);
109 | $('#info-arch').html(step.architecture);
110 | $('#info-url').html(step.documentURL);
111 | $('#info-url').attr("href", step.documentURL);
112 | $('#info-duration').html(step.duration + ' secs.');
113 | $('#info-start-time').html(moment(new Date(step.startTimestamp * 1000)).format(timestampFormat));
114 | $('#info-end-time').html(moment(new Date(step.endTimestamp * 1000)).format(timestampFormat));
115 | $('#info-time-trace-file').html(step.clangTimeTraceFile);
116 | $('#info-time-trace-file').attr("href", step.clangTimeTraceFile);
117 | showStepErrors(step);
118 | showStepWarnings(step);
119 | showSwiftFunctionTimes(step);
120 | showSwiftTypeCheckTimes(step);
121 | }
122 | }
123 |
124 | function loadStep() {
125 | const stepId = getRequestedStepId();
126 | if (stepId != null) {
127 | const steps = buildData.filter(function (step) {
128 | return stepId == step.identifier;
129 | });
130 | return steps[0];
131 | }
132 | return null;
133 | }
134 |
135 | function getRequestedStepId() {
136 | let name = "step"
137 | if (name = (new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)')).exec(location.search)) {
138 | return decodeURIComponent(name[1]);
139 | } else {
140 | return null;
141 | }
142 | }
143 |
144 | function showStepErrors(step) {
145 | const errorLegend = step.errorCount > 1 ? " errors in " : " error in ";
146 | const summaries = incidentTemplate({ "count": step.errorCount + errorLegend, "summary": step.signature, "details": step.errors });
147 | $('#errors-count').html(step.errorCount);
148 | $('#errors-summary').html(summaries);
149 | }
150 |
151 | function showStepWarnings(step) {
152 | const warningLegend = step.warningCount > 1 ? " warnings in " : " warning in ";
153 | const summaries = incidentTemplate({ "count": step.warningCount + warningLegend, "summary": step.signature, "details": step.warnings });
154 | $('#warnings-count').html(step.warningCount);
155 | $('#warnings-summary').html(summaries);
156 | }
157 |
158 | function showSwiftFunctionTimes(step) {
159 | if (step.detailStepType === 'swiftCompilation') {
160 | $('#functions-row').show();
161 | if (step.swiftFunctionTimes && step.swiftFunctionTimes.length > 0) {
162 | const cumulativeFunctions = step.swiftFunctionTimes.map(function(f) {
163 | f.cumulative = Math.round(f.occurrences * f.durationMS * 100) / 100;
164 | return f;
165 | });
166 | const functions = swiftFunctionTemplate({"functions": cumulativeFunctions});
167 | $('#functions-summary').html(functions);
168 | } else {
169 | $('#functions-summary').html(swiftFunctionWarning);
170 | }
171 |
172 | } else {
173 | $('#functions-row').hide();
174 | }
175 | }
176 |
177 | function showSwiftTypeCheckTimes(step) {
178 | if (step.detailStepType === 'swiftCompilation') {
179 | $('#typechecks-row').show();
180 | if (step.swiftTypeCheckTimes && step.swiftTypeCheckTimes.length > 0) {
181 | const cumulativeFunctions = step.swiftTypeCheckTimes.map(function(f) {
182 | f.cumulative = Math.round(f.occurrences * f.durationMS * 100) / 100;
183 | return f;
184 | });
185 | const functions = swiftTypeCheckTemplate({"functions": cumulativeFunctions});
186 | $('#typechecks-summary').html(functions);
187 | } else {
188 | $('#typechecks-summary').html(swiftTypeCheckWarning);
189 | }
190 |
191 | } else {
192 | $('#typechecks-row').hide();
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/XCLogParserError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public enum XCLogParserError: LocalizedError {
23 | case invalidLogHeader(String)
24 | case invalidLine(String)
25 | case errorCreatingReport(String)
26 | case wrongLogManifestFile(String, String)
27 | case parseError(String)
28 |
29 | public var errorDescription: String? { return description }
30 | }
31 |
32 | extension XCLogParserError: CustomStringConvertible {
33 | public var description: String {
34 | switch self {
35 | case .invalidLogHeader(let path):
36 | return "The file in \(path) is not a valid SLF log"
37 | case .invalidLine(let line):
38 | return "The line \(line) doesn't seem like a valid SLF line"
39 | case .errorCreatingReport(let error):
40 | return "Can't create the report: \(error)"
41 | case .wrongLogManifestFile(let path, let error):
42 | return "There was an error reading the latest build time " +
43 | " from the file \(path). Error: \(error)"
44 | case .parseError(let message):
45 | return "Error parsing the log: \(message)"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/commands/Action.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Actions supported by the library
23 | public enum Action {
24 |
25 | /// Dumps the content of an .xcactivitylog as a JSON document
26 | case dump(options: ActionOptions)
27 |
28 | /// Dumps the content of the LogManifest plist of a Log directory as a JSON Document
29 | case manifest(options: ActionOptions)
30 |
31 | /// Parses the content of an .xcactivitylog and generates different `BuildReport`s
32 | case parse(options: ActionOptions)
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/commands/ActionOptions.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Represents the options of an Action
23 | public struct ActionOptions {
24 |
25 | /// The `Reporter` to use
26 | public let reporter: Reporter
27 |
28 | /// The outputPath to write the report to.
29 | /// If empty, the report will be written to the StandardOutput
30 | public let outputPath: String
31 |
32 | /// Used for actions involving the .xcactivitylog.
33 | /// If true, the username will be redacted from the paths in the log.
34 | /// Used to protect the privacy of the users.
35 | public let redacted: Bool
36 |
37 | /// Used for actions involving the .xcactivitylog.
38 | /// If true, build specific information will be removed from the logs.
39 | /// Useful for grouping logs by its content.
40 | public let withoutBuildSpecificInformation: Bool
41 |
42 | /// Used in Parse actions. The current parsers use the host name to create a unique build identifier
43 | /// With this option, a user can override it and provide a name that will be used in that identifier.
44 | public let machineName: String?
45 |
46 | /// The rootOutput will generate the HTML output in the given folder
47 | public let rootOutput: String
48 |
49 | /// If true, the parse command won't add the Warnings details to the output.
50 | public let omitWarningsDetails: Bool
51 |
52 | /// If true, the parse command won't add the Notes details to the output.
53 | public let omitNotesDetails: Bool
54 |
55 | /// If true, tasks with more than a 100 issues (warnings, errors, notes) will be truncated to a 100
56 | public let truncLargeIssues: Bool
57 |
58 | public init(reporter: Reporter,
59 | outputPath: String,
60 | redacted: Bool,
61 | withoutBuildSpecificInformation: Bool,
62 | machineName: String? = nil,
63 | rootOutput: String = "",
64 | omitWarningsDetails: Bool = false,
65 | omitNotesDetails: Bool = false,
66 | truncLargeIssues: Bool = false
67 | ) {
68 | self.reporter = reporter
69 | self.outputPath = outputPath
70 | self.redacted = redacted
71 | self.withoutBuildSpecificInformation = withoutBuildSpecificInformation
72 | self.machineName = machineName
73 | self.rootOutput = rootOutput
74 | self.omitWarningsDetails = omitWarningsDetails
75 | self.omitNotesDetails = omitNotesDetails
76 | self.truncLargeIssues = truncLargeIssues
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/commands/Command.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Command that xclogparser can process.
23 | public struct Command {
24 | let logOptions: LogOptions
25 | let action: Action
26 |
27 | /// - param logOptions: An instance of `LogOptions` used to located the Logs folder
28 | /// - param action: The `Action` to perform
29 | public init(logOptions: LogOptions, action: Action) {
30 | self.logOptions = logOptions
31 | self.action = action
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/commands/CommandHandler.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct CommandHandler {
23 |
24 | let logFinder = LogFinder()
25 | let activityLogParser = ActivityParser()
26 |
27 | public init() { }
28 |
29 | public func handle(command: Command) throws {
30 | switch command.action {
31 | case .dump(let options):
32 | let logURL = try logFinder.findLatestLogWithLogOptions(command.logOptions)
33 | try handleDump(fromLogURL: logURL, options: options)
34 | case .manifest(let options):
35 | try handleManifestWithLogOptions(command.logOptions, andActionOptions: options)
36 | case .parse(let options):
37 | let logURL = try logFinder.findLatestLogWithLogOptions(command.logOptions)
38 | try handleParse(fromLogURL: logURL, options: options)
39 | }
40 | }
41 |
42 | func handleManifestWithLogOptions(_ logOptions: LogOptions,
43 | andActionOptions actionOptions: ActionOptions) throws {
44 | let logManifest = LogManifest()
45 | let logManifestEntries = try logManifest.getWithLogOptions(logOptions)
46 | let reporterOutput = ReporterOutputFactory.makeReporterOutput(path: actionOptions.outputPath)
47 | let logReporter = actionOptions.reporter.makeLogReporter()
48 | try logReporter.report(build: logManifestEntries, output: reporterOutput, rootOutput: actionOptions.rootOutput)
49 | }
50 |
51 | func handleDump(fromLogURL logURL: URL, options: ActionOptions) throws {
52 | let activityLog = try activityLogParser.parseActivityLogInURL(logURL,
53 | redacted: options.redacted,
54 | withoutBuildSpecificInformation: options.withoutBuildSpecificInformation) // swiftlint:disable:this line_length
55 | let reporterOutput = ReporterOutputFactory.makeReporterOutput(path: options.outputPath)
56 | let logReporter = options.reporter.makeLogReporter()
57 | try logReporter.report(build: activityLog, output: reporterOutput, rootOutput: options.rootOutput)
58 | }
59 |
60 | func handleParse(fromLogURL logURL: URL, options: ActionOptions) throws {
61 | let activityLog = try activityLogParser.parseActivityLogInURL(logURL,
62 | redacted: options.redacted,
63 | withoutBuildSpecificInformation:
64 | options.withoutBuildSpecificInformation)
65 |
66 | let buildParser = ParserBuildSteps(machineName: options.machineName,
67 | omitWarningsDetails: options.omitWarningsDetails,
68 | omitNotesDetails: options.omitNotesDetails,
69 | truncLargeIssues: options.truncLargeIssues)
70 | let buildSteps = try buildParser.parse(activityLog: activityLog)
71 | let reporterOutput = ReporterOutputFactory.makeReporterOutput(path: options.outputPath)
72 | let logReporter = options.reporter.makeLogReporter()
73 | try logReporter.report(build: buildSteps, output: reporterOutput, rootOutput: options.rootOutput)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/commands/LogOptions.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Represents the strategy to locate a Xcode Log resource
23 | public struct LogOptions {
24 | /// The name of the Xcode project
25 | let projectName: String
26 |
27 | /// The path to an .xcworkspace
28 | let xcworkspacePath: String
29 |
30 | /// The path to an .xcodeprojPath
31 | let xcodeprojPath: String
32 |
33 | /// The path to the DerivedData directory
34 | let derivedDataPath: String
35 |
36 | /// The path to an .xcactivitylog file
37 | let xcactivitylogPath: String
38 |
39 | /// The path to a LogManifest.plist file
40 | let logManifestPath: String
41 |
42 | /// Use strict Xcode project naming.
43 | let strictProjectName: Bool
44 |
45 | /// Computed property, return the xcworkspacePath if not empty or
46 | /// the xcodeprojPath if xcworkspacePath is empty
47 | var projectLocation: String {
48 | return xcworkspacePath.isEmpty ? xcodeprojPath : xcworkspacePath
49 | }
50 |
51 | public init(projectName: String,
52 | xcworkspacePath: String,
53 | xcodeprojPath: String,
54 | derivedDataPath: String,
55 | xcactivitylogPath: String,
56 | strictProjectName: Bool = false) {
57 | self.projectName = projectName
58 | self.xcworkspacePath = xcworkspacePath
59 | self.xcodeprojPath = xcodeprojPath
60 | self.derivedDataPath = derivedDataPath
61 | self.xcactivitylogPath = xcactivitylogPath
62 | self.logManifestPath = String()
63 | self.strictProjectName = strictProjectName
64 | }
65 |
66 | public init(projectName: String,
67 | xcworkspacePath: String,
68 | xcodeprojPath: String,
69 | derivedDataPath: String,
70 | logManifestPath: String,
71 | strictProjectName: Bool = false) {
72 | self.projectName = projectName
73 | self.xcworkspacePath = xcworkspacePath
74 | self.xcodeprojPath = xcodeprojPath
75 | self.derivedDataPath = derivedDataPath
76 | self.logManifestPath = logManifestPath
77 | self.xcactivitylogPath = String()
78 | self.strictProjectName = strictProjectName
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/commands/Version.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct Version {
23 |
24 | public static let current = "0.2.42"
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/extensions/ArrayExtension.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension Array where Element: Hashable {
23 |
24 | func removingDuplicates() -> [Element] {
25 | var addedDict = [Element: Bool]()
26 | return filter {
27 | addedDict.updateValue(true, forKey: $0) == nil
28 | }
29 | }
30 |
31 | }
32 |
33 | extension Array where Element: Notice {
34 |
35 | func getWarnings() -> [Notice] {
36 | return filter {
37 | $0.type == .swiftWarning ||
38 | $0.type == .clangWarning ||
39 | $0.type == .projectWarning ||
40 | $0.type == .analyzerWarning ||
41 | $0.type == .interfaceBuilderWarning ||
42 | $0.type == .deprecatedWarning
43 | }
44 | }
45 |
46 | func getErrors() -> [Notice] {
47 | return filter {
48 | $0.type == .swiftError ||
49 | $0.type == .error ||
50 | $0.type == .clangError ||
51 | $0.type == .linkerError ||
52 | $0.type == .packageLoadingError ||
53 | $0.type == .scriptPhaseError ||
54 | $0.type == .failedCommandError
55 | }
56 | }
57 |
58 | func getNotes() -> [Notice] {
59 | return filter {
60 | $0.type == .note
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/extensions/EncodableExtension.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension Encodable {
23 |
24 | func toJSON() throws -> Data {
25 | let encoder = JSONEncoder()
26 | encoder.outputFormatting = .prettyPrinted
27 | return try encoder.encode(self)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/extensions/NSRegularExpressionExtension.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension NSRegularExpression {
23 |
24 | static func fromPattern(_ pattern: String) -> NSRegularExpression? {
25 | do {
26 | return try NSRegularExpression(pattern: pattern)
27 | } catch {
28 | return nil
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/extensions/URLExtension.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension URL {
23 |
24 | static var homeDir: URL? {
25 | if #available(OSX 10.12, *) {
26 | return FileManager.default.homeDirectoryForCurrentUser
27 | } else {
28 | return FileManager.default.urls(for: .userDirectory, in: .userDomainMask).first
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/lexer/LexRedactor.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public class LexRedactor: LogRedactor {
23 | private static let redactedTemplate = "/Users//"
24 | private lazy var userDirRegex: NSRegularExpression? = {
25 | do {
26 | return try NSRegularExpression(pattern: "/Users/([^/]+)/?")
27 | } catch {
28 | return nil
29 | }
30 | }()
31 | public var userDirToRedact: String?
32 |
33 | public init() {
34 | }
35 |
36 | public func redactUserDir(string: String) -> String {
37 | guard let regex = userDirRegex else {
38 | return string
39 | }
40 | if let userDirToRedact = userDirToRedact {
41 | return string.replacingOccurrences(of: userDirToRedact, with: Self.redactedTemplate)
42 | } else {
43 | guard let firstMatch = regex.firstMatch(in: string,
44 | options: [],
45 | range: NSRange(location: 0, length: string.count)) else {
46 | return string
47 | }
48 | let userDir = string.substring(firstMatch.range)
49 | userDirToRedact = userDir
50 | return string.replacingOccurrences(of: userDir, with: Self.redactedTemplate)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/lexer/LexerModel.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public enum TokenType: String, CaseIterable {
23 | case int = "#"
24 | case className = "%"
25 | case classNameRef = "@"
26 | case string = "\""
27 | case double = "^"
28 | case null = "-"
29 | case list = "("
30 | case json = "*"
31 |
32 | static func all() -> String {
33 | return TokenType.allCases.reduce(String()) {
34 | return "\($0)\($1.rawValue)"
35 | }
36 | }
37 | }
38 |
39 | public enum Token: CustomDebugStringConvertible, Equatable {
40 | case int(UInt64)
41 | case className(String)
42 | case classNameRef(String)
43 | case string(String)
44 | case double(Double)
45 | case null
46 | case list(Int)
47 | case json(String)
48 | }
49 |
50 | extension Token {
51 | public var debugDescription: String {
52 | switch self {
53 | case .int(let value):
54 | return "[type: int, value: \(value)]"
55 | case .className(let name):
56 | return "[type: className, name: \"\(name)\"]"
57 | case .classNameRef(let name):
58 | return "[type: classNameRef, className: \"\(name)\"]"
59 | case .string(let value):
60 | return "[type: string, value: \"\(value)\"]"
61 | case .double(let value):
62 | return "[type: double, value: \(value)]"
63 | case .null:
64 | return "[type: nil]"
65 | case .list(let count):
66 | return "[type: list, count: \(count)]"
67 | case .json(let json):
68 | return "[type: json, value: \(json)]"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/lexer/LogRedactor.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | public protocol LogRedactor {
21 | /// Predefined (or inferrred during redation process) user home path.
22 | /// Introduced for better performance.
23 | var userDirToRedact: String? {get set}
24 |
25 | /// Redacts a string by replacing sensitive username path with a template
26 | /// - parameter string: The string to redact
27 | /// - returns: Redacted text with
28 | func redactUserDir(string: String) -> String
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/lexer/String+BuildSpecificInformationRemoval.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Methods on `String` that remove build specific information from the log.
23 | ///
24 | /// This can be useful if we want to group logs by its content in order to indicate
25 | /// how often particular log occurs.
26 | extension String {
27 | /// Removes autogenerated build identifier in built product path.
28 | ///
29 | /// Example: "DerivedData/Product-bolnckhlbzxpxoeyfujluasoupft/Build" becomes "DerivedData/Product/Build".
30 | func removeProductBuildIdentifier() -> String {
31 | do {
32 | var mutableSelf = self
33 | let regularExpression = try NSRegularExpression(pattern: "/DerivedData/(.*)-(.*)/Build/")
34 | regularExpression.enumerateMatches(in: self,
35 | options: [],
36 | range: NSRange(location: 0, length: count)) { match, _, _ in
37 | if let match = match, match.numberOfRanges == 3 {
38 | let buildIdentifier = self.substring(match.range(at: 2))
39 | mutableSelf = mutableSelf.replacingOccurrences(of: "-" + buildIdentifier, with: "")
40 | }
41 | }
42 | return mutableSelf
43 | } catch {
44 | return self
45 | }
46 | }
47 |
48 | /// Removes hexadecimal numbers from the log and puts `` instead.
49 | ///
50 | /// Example: "NSUnderlyingError=0x7fcdc8712290" becomes "NSUnderlyingError=".
51 | func removeHexadecimalNumbers() -> String {
52 | do {
53 | var mutableSelf = self
54 | let regularExpression = try NSRegularExpression(pattern: "0[xX][0-9a-fA-F]+")
55 | regularExpression.enumerateMatches(in: self,
56 | options: [],
57 | range: NSRange(location: 0, length: count)) { match, _, _ in
58 | if let match = match {
59 | let hexadecimalNumber = self.substring(match.range(at: 0))
60 | mutableSelf = mutableSelf.replacingOccurrences(of: hexadecimalNumber, with: "")
61 | }
62 | }
63 | return mutableSelf
64 | } catch {
65 | return self
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/loglocation/LogError.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Errors thrown by the LogFinder
23 | public enum LogError: LocalizedError {
24 | case noDerivedDataFound
25 | case noLogFound(dir: String)
26 | case xcodeBuildError(String)
27 | case readingFile(String)
28 | case invalidFile(String)
29 | case noLogManifestFound(dir: String)
30 | case invalidLogManifest(String)
31 |
32 | public var errorDescription: String? { return description }
33 |
34 | }
35 |
36 | extension LogError: CustomStringConvertible {
37 |
38 | public var description: String {
39 | switch self {
40 | case .noDerivedDataFound:
41 | return "We couldn't find the derivedData directory. " +
42 | "If you use a custom derivedData directory, use the --derived_data option to pass it. "
43 | case .noLogFound(let dir):
44 | return "We couldn't find a log in the directory \(dir). " +
45 | "If the log is in a custom derivedData dir, use the --derived_data option. " +
46 | "You can also pass the full path to the xcactivity log with the --file option"
47 | case .xcodeBuildError(let error):
48 | return error
49 | case .readingFile(let path):
50 | return "Can't read file \(path)"
51 | case .invalidFile(let path):
52 | return "\(path) is not a valid xcactivitylog file"
53 | case .noLogManifestFound(let path):
54 | return "We couldn't find a logManifest in the path \(path). " +
55 | "If the LogManifest is in a custom derivedData directory, use the --derived_data option."
56 | case .invalidLogManifest(let path):
57 | return "\(path) is not a valid LogManifest file"
58 | }
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/loglocation/LogLoader.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 | import Gzip
22 |
23 | public struct LogLoader {
24 |
25 | func loadFromURL(_ url: URL) throws -> String {
26 | do {
27 | let data = try Data(contentsOf: url)
28 | let unzipped = try data.gunzipped()
29 | let string: String? = unzipped.withUnsafeBytes { pointer in
30 | guard let charPointer = pointer
31 | .assumingMemoryBound(to: CChar.self)
32 | .baseAddress
33 | else {
34 | return nil
35 | }
36 |
37 | return String(cString: charPointer, encoding: .ascii)
38 | }
39 | guard let contents = string else {
40 | throw LogError.readingFile(url.path)
41 | }
42 | return contents
43 | } catch {
44 | throw LogError.invalidFile(url.path)
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/logmanifest/LogManifest.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Parses a LogManifest.plist file.
23 | /// That file has a list of the existing Xcode Logs inside a Derived Data's project directory
24 | public struct LogManifest {
25 |
26 | public init() {}
27 |
28 | public func getWithLogOptions(_ logOptions: LogOptions) throws -> [LogManifestEntry] {
29 | let logFinder = LogFinder()
30 | let logManifestURL = try logFinder.findLogManifestWithLogOptions(logOptions)
31 | let logManifestDictionary = try getDictionaryFromURL(logManifestURL)
32 | return try parse(dictionary: logManifestDictionary, atPath: logManifestURL.path)
33 | }
34 |
35 | public func parse(dictionary: NSDictionary, atPath path: String) throws -> [LogManifestEntry] {
36 | guard let logs = dictionary["logs"] as? [String: [String: Any]] else {
37 | throw LogError.invalidLogManifest("The file at \(path) is not a valid " +
38 | "LogManifest file.")
39 | }
40 | return try logs.compactMap { entry -> LogManifestEntry? in
41 | guard
42 | let fileName = entry.value["fileName"] as? String,
43 | let title = entry.value["title"] as? String,
44 | let uniqueIdentifier = entry.value["uniqueIdentifier"] as? String,
45 | let scheme = entry.value["schemeIdentifier-schemeName"] as? String,
46 | let timeStartedRecording = entry.value["timeStartedRecording"] as? Double,
47 | let timeStoppedRecording = entry.value["timeStoppedRecording"] as? Double,
48 | let className = entry.value["className"] as? String,
49 | let type = LogManifestEntryType.buildFromClassName(className)
50 | else {
51 | throw LogError.invalidLogManifest("The file at \(path) is not a valid " +
52 | "LogManifest file.")
53 | }
54 | let startDate = Date(timeIntervalSinceReferenceDate: timeStartedRecording)
55 | let endDate = Date(timeIntervalSinceReferenceDate: timeStoppedRecording)
56 | let timestampStart = Int(startDate.timeIntervalSince1970.rounded())
57 | let timestampEnd = Int(endDate.timeIntervalSince1970.rounded())
58 |
59 | return LogManifestEntry(uniqueIdentifier: uniqueIdentifier,
60 | title: title,
61 | scheme: scheme,
62 | fileName: fileName,
63 | timestampStart: timestampStart,
64 | timestampEnd: timestampEnd,
65 | duration: timestampEnd - timestampStart,
66 | type: type)
67 | }.sorted(by: { lhs, rhs -> Bool in
68 | return lhs.timestampStart > rhs.timestampStart
69 | })
70 | }
71 |
72 | private func getDictionaryFromURL(_ logManifestURL: URL) throws -> NSDictionary {
73 | guard let logContents = NSDictionary(contentsOfFile: logManifestURL.path) else {
74 | throw LogError.invalidLogManifest("The file at \(logManifestURL.path) is not a valid " +
75 | "LogManifest file.")
76 | }
77 | return logContents
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/logmanifest/LogManifestModel.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public enum LogManifestEntryType: String, Encodable {
23 | case xcode
24 | case xcodebuild
25 |
26 | private static let xcodeLogClassName = "IDEActivityLogSection"
27 | private static let xcodebuildLogClassName = "IDECommandLineBuildLog"
28 |
29 | public static func buildFromClassName(_ className: String) -> LogManifestEntryType? {
30 | if className == xcodeLogClassName {
31 | return .xcode
32 | }
33 | if className == xcodebuildLogClassName {
34 | return .xcodebuild
35 | }
36 | return nil
37 | }
38 | }
39 |
40 | public struct LogManifestEntry: Encodable {
41 | public let uniqueIdentifier: String
42 | public let title: String
43 | public let scheme: String
44 | public let fileName: String
45 | public let timestampStart: Int
46 | public let timestampEnd: Int
47 | public let duration: Int
48 | public let type: LogManifestEntryType
49 |
50 | public init(uniqueIdentifier: String, title: String, scheme: String, fileName: String,
51 | timestampStart: Int, timestampEnd: Int, duration: Int, type: LogManifestEntryType) {
52 | self.uniqueIdentifier = uniqueIdentifier
53 | self.title = title
54 | self.scheme = scheme
55 | self.fileName = fileName
56 | self.timestampStart = timestampStart
57 | self.timestampEnd = timestampEnd
58 | self.duration = duration
59 | self.type = type
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/output/FileOutput.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 | import PathKit
22 |
23 | public final class FileOutput: ReporterOutput {
24 |
25 | let path: String
26 |
27 | public init(path: String) {
28 | let absolutePath = Path(path).absolute()
29 | self.path = absolutePath.string
30 | }
31 |
32 | public func write(report: Any) throws {
33 | switch report {
34 | case let data as Data:
35 | try write(data: data)
36 | case let tokens as [Token]:
37 | try write(tokens: tokens)
38 | default:
39 | throw XCLogParserError.errorCreatingReport("Can't write the report. Type not supported: " +
40 | "\(type(of: report)).")
41 | }
42 | }
43 |
44 | private func write(data: Data) throws {
45 | let fileManager = FileManager.default
46 | if fileManager.fileExists(atPath: path) {
47 | throw XCLogParserError.errorCreatingReport("Can't write the report to \(path). A file already exists.")
48 | }
49 | let url = URL(fileURLWithPath: path)
50 | try data.write(to: url)
51 | print("File written to \(path)")
52 | }
53 |
54 | private func write(tokens: [Token]) throws {
55 | let fileManager = FileManager.default
56 | guard fileManager.fileExists(atPath: path) == false
57 | else {
58 | throw XCLogParserError.errorCreatingReport("Can't write the report to \(path). A file already exists.")
59 | }
60 | fileManager.createFile(atPath: path, contents: nil, attributes: nil)
61 | guard let fileHandle = FileHandle.init(forWritingAtPath: path) else {
62 | throw XCLogParserError.errorCreatingReport("Can't write the report to \(path). File can't be created.")
63 | }
64 | defer {
65 | fileHandle.closeFile()
66 | }
67 | for token in tokens {
68 | guard let data = "\(token)\n".data(using: .utf8) else {
69 | throw XCLogParserError.errorCreatingReport("Can't write the report to \(path)." +
70 | "Token can't be serialized \(token).")
71 | }
72 | fileHandle.write(data)
73 | }
74 | print("File written to \(path)")
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/output/ReporterOutput.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Protocol thar represents the output of a report
23 | public protocol ReporterOutput {
24 |
25 | /// Writes the report to the output
26 | func write(report: Any) throws
27 |
28 | }
29 |
30 | public struct ReporterOutputFactory {
31 |
32 | /// Creates a `ReporterOutput` based on the path passed
33 | /// - parameter path: If the path is empty, the reporter will be writter using `StandardOutput`
34 | /// If not, a `FileOutput` will be used
35 | /// - returns: An instance of `ReporterOutput` configured with the given path
36 | public static func makeReporterOutput(path: String?) -> ReporterOutput {
37 | if let path = path, path.isEmpty == false {
38 | return FileOutput(path: path)
39 | }
40 | return StandardOutput()
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/output/StandardOutput.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public final class StandardOutput: ReporterOutput {
23 |
24 | public init() {}
25 |
26 | public func write(report: Any) throws {
27 | switch report {
28 | case let tokens as [Token]:
29 | tokens.forEach { (token) in
30 | print(token)
31 | }
32 | case let data as Data:
33 | write(data: data)
34 | default:
35 | print("Type not supported \(type(of: report))")
36 | }
37 |
38 | }
39 |
40 | private func write(data: Data) {
41 | if let string = String(data: data, encoding: .utf8) {
42 | print(string)
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/BuildStatusSanitizer.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | class BuildStatusSanitizer {
23 | static func sanitize(originalStatus: String) -> String {
24 | let sanitizedStatus = originalStatus
25 | .replacingOccurrences(of: "Build", with: "")
26 | .replacingOccurrences(of: "Clean", with: "")
27 | .trimmingCharacters(in: .whitespacesAndNewlines)
28 | return sanitizedStatus
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/BuildStep+Parser.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public extension BuildStep {
23 |
24 | /// Flattens a group of swift compilations steps.
25 | ///
26 | /// When a Swift module is compiled with `whole module` option
27 | /// The parsed log looks like:
28 | /// - CompileSwiftTarget
29 | /// - CompileSwift
30 | /// - CompileSwift file1.swift
31 | /// - CompileSwift file2.swift
32 | /// This tasks removes the intermediate CompileSwift step and moves the substeps
33 | /// to the root:
34 | /// - CompileSwiftTarget
35 | /// - CompileSwift file1.swift
36 | /// - CompileSwift file2.swift
37 | /// - Returns: The build step with its swift substeps at the root level, and intermediate CompileSwift step removed.
38 | func moveSwiftStepsToRoot() -> BuildStep {
39 | var updatedSubSteps = subSteps
40 | for (index, subStep) in subSteps.enumerated() {
41 | if subStep.detailStepType == .swiftCompilation && subStep.subSteps.count > 0 {
42 | updatedSubSteps.remove(at: index)
43 | updatedSubSteps.append(contentsOf: subStep.subSteps)
44 | }
45 | }
46 | return with(subSteps: updatedSubSteps)
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/Contains.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct Contains {
23 |
24 | let str: String
25 |
26 | public init(_ str: String) {
27 | self.str = str.lowercased()
28 | }
29 |
30 | private func match(_ input: String) -> Bool {
31 | return input.lowercased().contains(str)
32 | }
33 | }
34 |
35 | extension Contains {
36 | static func ~= (contains: Contains, input: String) -> Bool {
37 | return contains.match(input)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/IDEActivityLogSection+Builders.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension IDEActivityLogSection {
23 |
24 | func with(messages newMessages: [IDEActivityLogMessage]) -> IDEActivityLogSection {
25 | return IDEActivityLogSection(sectionType: self.sectionType,
26 | domainType: self.domainType,
27 | title: self.title,
28 | signature: self.signature,
29 | timeStartedRecording: self.timeStartedRecording,
30 | timeStoppedRecording: self.timeStoppedRecording,
31 | subSections: self.subSections,
32 | text: self.text,
33 | messages: newMessages,
34 | wasCancelled: self.wasCancelled,
35 | isQuiet: self.isQuiet,
36 | wasFetchedFromCache: self.wasFetchedFromCache,
37 | subtitle: self.subtitle,
38 | location: self.location,
39 | commandDetailDesc: self.commandDetailDesc,
40 | uniqueIdentifier: self.uniqueIdentifier,
41 | localizedResultString: self.localizedResultString,
42 | xcbuildSignature: self.xcbuildSignature,
43 | attachments: self.attachments,
44 | unknown: self.unknown)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/LinkerStatistics.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Wrap the statistics data produced by ld64 linker (-print_statistics)
23 | public class LinkerStatistics: Encodable {
24 | public let totalMS: Double
25 |
26 | public let optionParsingMS: Double
27 | public let optionParsingPercent: Double
28 |
29 | public let objectFileProcessingMS: Double
30 | public let objectFileProcessingPercent: Double
31 |
32 | public let resolveSymbolsMS: Double
33 | public let resolveSymbolsPercent: Double
34 |
35 | public let buildAtomListMS: Double
36 | public let buildAtomListPercent: Double
37 |
38 | public let runPassesMS: Double
39 | public let runPassesPercent: Double
40 |
41 | public let writeOutputMS: Double
42 | public let writeOutputPercent: Double
43 |
44 | public let pageins: Int
45 | public let pageouts: Int
46 | public let faults: Int
47 |
48 | public let objectFiles: Int
49 | public let objectFilesBytes: Int
50 |
51 | public let archiveFiles: Int
52 | public let archiveFilesBytes: Int
53 |
54 | public let dylibFiles: Int
55 | public let wroteOutputFileBytes: Int
56 |
57 | public init(totalMS: Double,
58 | optionParsingMS: Double,
59 | optionParsingPercent: Double,
60 | objectFileProcessingMS: Double,
61 | objectFileProcessingPercent: Double,
62 | resolveSymbolsMS: Double,
63 | resolveSymbolsPercent: Double,
64 | buildAtomListMS: Double,
65 | buildAtomListPercent: Double,
66 | runPassesMS: Double,
67 | runPassesPercent: Double,
68 | writeOutputMS: Double,
69 | writeOutputPercent: Double,
70 | pageins: Int,
71 | pageouts: Int,
72 | faults: Int,
73 | objectFiles: Int,
74 | objectFilesBytes: Int,
75 | archiveFiles: Int,
76 | archiveFilesBytes: Int,
77 | dylibFiles: Int,
78 | wroteOutputFileBytes: Int) {
79 | self.totalMS = totalMS
80 | self.optionParsingMS = optionParsingMS
81 | self.optionParsingPercent = optionParsingPercent
82 | self.objectFileProcessingMS = objectFileProcessingMS
83 | self.objectFileProcessingPercent = objectFileProcessingPercent
84 | self.resolveSymbolsMS = resolveSymbolsMS
85 | self.resolveSymbolsPercent = resolveSymbolsPercent
86 | self.buildAtomListMS = buildAtomListMS
87 | self.buildAtomListPercent = buildAtomListPercent
88 | self.runPassesMS = runPassesMS
89 | self.runPassesPercent = runPassesPercent
90 | self.writeOutputMS = writeOutputMS
91 | self.writeOutputPercent = writeOutputPercent
92 | self.pageins = pageins
93 | self.pageouts = pageouts
94 | self.faults = faults
95 | self.objectFiles = objectFiles
96 | self.objectFilesBytes = objectFilesBytes
97 | self.archiveFiles = archiveFiles
98 | self.archiveFilesBytes = archiveFilesBytes
99 | self.dylibFiles = dylibFiles
100 | self.wroteOutputFileBytes = wroteOutputFileBytes
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/MachineNameReader.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Reads the name of the machine where this command is executed
23 | protocol MachineNameReader {
24 | var machineName: String? { get }
25 | }
26 |
27 | /// Implementation of `MachineReader` that uses the name of the host as the Machine name
28 | class MacOSMachineNameReader: MachineNameReader {
29 | var machineName: String? {
30 | return Host.current().localizedName
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/NoticeType.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// The type of a Notice
23 | public enum NoticeType: String, Codable {
24 |
25 | /// Notes
26 | case note
27 |
28 | /// A warning thrown by the Swift compiler
29 | case swiftWarning
30 |
31 | /// A warning thrown by the C compiler
32 | case clangWarning
33 |
34 | /// A warning at a project level. For instance:
35 | /// "Warning Swift 3 mode has been deprecated and will be removed in a later version of Xcode"
36 | case projectWarning
37 |
38 | /// An error in a non-compilation step. For instance creating a directory or running a shell script phase
39 | case error
40 |
41 | /// An error thrown by the Swift compiler
42 | case swiftError
43 |
44 | /// An error thrown by the C compiler
45 | case clangError
46 |
47 | /// A warning returned by Xcode static analyzer
48 | case analyzerWarning
49 |
50 | /// A warning inside an Interface Builder file
51 | case interfaceBuilderWarning
52 |
53 | /// A warning about the usage of a deprecated API
54 | case deprecatedWarning
55 |
56 | /// Error thrown by the Linker
57 | case linkerError
58 |
59 | /// Error loading Swift Packages
60 | case packageLoadingError
61 |
62 | /// Error running a Build Phase's script
63 | case scriptPhaseError
64 |
65 | /// Failed command error (e.g. ValidateEmbeddedBinary, CodeSign)
66 | case failedCommandError
67 |
68 | // swiftlint:disable:next cyclomatic_complexity
69 | public static func fromTitle(_ title: String) -> NoticeType? {
70 | switch title {
71 | case "Swift Compiler Warning":
72 | return .swiftWarning
73 | case "Notice":
74 | return .note
75 | case "Swift Compiler Error":
76 | return .swiftError
77 | case Prefix("Lexical"), Suffix("Semantic Issue"), "Parse Issue", "Uncategorized":
78 | return .clangError
79 | case Suffix("Deprecations"):
80 | return .deprecatedWarning
81 | case "Warning", "Apple Mach-O Linker Warning", "Target Integrity":
82 | return .projectWarning
83 | case Suffix("Error"):
84 | return .error
85 | case Suffix("Notice"):
86 | return .note
87 | case Prefix("/* com.apple.ibtool.document.warnings */"):
88 | return .interfaceBuilderWarning
89 | case "Package Loading":
90 | return .packageLoadingError
91 | case Contains("Command PhaseScriptExecution"):
92 | return .scriptPhaseError
93 | case Prefix("error: Swiftc"):
94 | return .swiftError
95 | case Suffix("failed with a nonzero exit code"):
96 | return .failedCommandError
97 | default:
98 | return .note
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/Prefix.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct Prefix {
23 |
24 | let prefix: String
25 |
26 | public init(_ prefix: String) {
27 | self.prefix = prefix.lowercased()
28 | }
29 |
30 | private func match(_ input: String) -> Bool {
31 | return input.lowercased().starts(with: prefix)
32 | }
33 | }
34 |
35 | extension Prefix {
36 | static func ~= (prefix: Prefix, input: String) -> Bool {
37 | return prefix.match(input)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/StringExtension.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension String {
23 |
24 | func substring(_ range: NSRange) -> String {
25 | guard let stringRange = Range(range, in: self) else {
26 | return ""
27 | }
28 | return String(self[stringRange])
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/Suffix.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct Suffix {
23 |
24 | let suffix: String
25 |
26 | public init(_ suffix: String) {
27 | self.suffix = suffix.lowercased()
28 | }
29 |
30 | private func match(_ input: String) -> Bool {
31 | return input.lowercased().hasSuffix(suffix)
32 | }
33 | }
34 |
35 | extension Suffix {
36 | static func ~= (suffix: Suffix, input: String) -> Bool {
37 | return suffix.match(input)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/SwiftCompilerFunctionTimeOptionParser.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Parses Swift Function times generated by `swiftc`
23 | /// if you pass the flags `-Xfrontend -debug-time-function-bodies`
24 | class SwiftCompilerFunctionTimeOptionParser: SwiftCompilerTimeOptionParser {
25 |
26 | private static let compilerFlag = "-debug-time-function-bodies"
27 |
28 | func hasCompilerFlag(commandDesc: String) -> Bool {
29 | commandDesc.range(of: Self.compilerFlag) != nil
30 | }
31 |
32 | func parse(from commands: [String: Int]) -> [String: [SwiftFunctionTime]] {
33 | let functionsPerFile = commands.compactMap { parse(command: $0.key, occurrences: $0.value) }
34 | .joined().reduce([:]) { (functionsPerFile, functionTime)
35 | -> [String: [SwiftFunctionTime]] in
36 | var functionsPerFile = functionsPerFile
37 | if var functions = functionsPerFile[functionTime.file] {
38 | functions.append(functionTime)
39 | functionsPerFile[functionTime.file] = functions
40 | } else {
41 | functionsPerFile[functionTime.file] = [functionTime]
42 | }
43 | return functionsPerFile
44 | }
45 | return functionsPerFile
46 | }
47 |
48 | private func parse(command: String, occurrences: Int) -> [SwiftFunctionTime]? {
49 | let functions: [SwiftFunctionTime] = command.components(separatedBy: "\r").compactMap { commandLine in
50 |
51 | // 0.14ms /users/mnf/project/SomeFile.swift:10:12 someMethod(param:)
52 | let parts = commandLine.components(separatedBy: "\t")
53 |
54 | guard parts.count == 3 else {
55 | return nil
56 | }
57 |
58 | // 0.14ms
59 | let duration = parseCompileDuration(parts[0])
60 |
61 | // /users/mnf/project/SomeFile.swift:10:12
62 | let fileAndLocation = parts[1]
63 | guard let (file, line, column) = parseNameAndLocation(from: fileAndLocation) else {
64 | return nil
65 | }
66 |
67 | // someMethod(param:)
68 | let signature = parts[2]
69 |
70 | return SwiftFunctionTime(file: file,
71 | durationMS: duration,
72 | startingLine: line,
73 | startingColumn: column,
74 | signature: signature,
75 | occurrences: occurrences)
76 | }
77 |
78 | return functions
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/SwiftCompilerParser.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Parses `swiftc` compiler times generated by
23 | /// `-Xfrontend` flags such as `-debug-time-function-bodies` and `-debug-time-expression-type-checking`
24 | public class SwiftCompilerParser {
25 |
26 | /// Array to store the text of the log sections and their commandDetailDesc
27 | var commands = [(String, String)]()
28 |
29 | /// Dictionary to store the function times found per filepath
30 | var functionsPerFile: [String: [SwiftFunctionTime]]?
31 |
32 | /// Dictionary to store the type checker times found per filepath
33 | var typeChecksPerFile: [String: [SwiftTypeCheck]]?
34 |
35 | let functionTimes = SwiftCompilerFunctionTimeOptionParser()
36 |
37 | let typeCheckTimes = SwiftCompilerTypeCheckOptionParser()
38 |
39 | public func addLogSection(_ logSection: IDEActivityLogSection) {
40 | commands.append((logSection.text, logSection.commandDetailDesc))
41 | }
42 |
43 | /// Checks the `commandDetailDesc` stored by the function `addLogSection`
44 | /// to know if the command `text` contains data about the swift function times.
45 | /// If there is data, the `text` wit that raw data is returned as part of a Set.
46 | ///
47 | /// - Returns: a Dictionary of Strings with the raw Swift compiler times data as key and
48 | /// the number of ocurrences as value
49 | public func findRawSwiftTimes() -> [String: Int] {
50 | let insertQueue = DispatchQueue(label: "swift_function_times_queue")
51 | var textsAndOccurrences: [String: Int] = [:]
52 | DispatchQueue.concurrentPerform(iterations: commands.count) { index in
53 | let (rawFunctionTimes, commandDesc) = commands[index]
54 |
55 | let hasCompilerFlag = functionTimes.hasCompilerFlag(commandDesc: commandDesc)
56 | || typeCheckTimes.hasCompilerFlag(commandDesc: commandDesc)
57 |
58 | guard hasCompilerFlag && rawFunctionTimes.isEmpty == false else {
59 | return
60 | }
61 | insertQueue.sync {
62 | let outputOccurrence = (textsAndOccurrences[rawFunctionTimes] ?? 0) + 1
63 | textsAndOccurrences[rawFunctionTimes] = outputOccurrence
64 | }
65 | }
66 | return textsAndOccurrences
67 | }
68 |
69 | /// Parses the swift function times and store them internally
70 | public func parse() {
71 | let rawTexts = findRawSwiftTimes()
72 | functionsPerFile = functionTimes.parse(from: rawTexts)
73 | typeChecksPerFile = typeCheckTimes.parse(from: rawTexts)
74 | }
75 |
76 | public func findFunctionTimesForFilePath(_ filePath: String) -> [SwiftFunctionTime]? {
77 | // File paths found in IDEActivityLogSection.text are unescaped
78 | // so percent encoding needs to be removed from filePath
79 | guard let unescapedFilePath = filePath.removingPercentEncoding else {
80 | return nil
81 | }
82 | return functionsPerFile?[unescapedFilePath]
83 | }
84 |
85 | public func findTypeChecksForFilePath(_ filePath: String) -> [SwiftTypeCheck]? {
86 | // File paths found in IDEActivityLogSection.text are unescaped
87 | // so percent encoding needs to be removed from filePath
88 | guard let unescapedFilePath = filePath.removingPercentEncoding else {
89 | return nil
90 | }
91 | return typeChecksPerFile?[unescapedFilePath]
92 | }
93 |
94 | public func hasFunctionTimes() -> Bool {
95 | guard let functionsPerFile = functionsPerFile else {
96 | return false
97 | }
98 | return functionsPerFile.isEmpty == false
99 | }
100 |
101 | public func hasTypeChecks() -> Bool {
102 | guard let typeChecksPerFile = typeChecksPerFile else {
103 | return false
104 | }
105 | return typeChecksPerFile.isEmpty == false
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/SwiftCompilerTimeOptionParser.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Parses `swiftc` commands for time compiler outputs
23 | protocol SwiftCompilerTimeOptionParser {
24 |
25 | associatedtype SwiftcOption
26 |
27 | /// Returns true if the compiler command included the flag to generate
28 | /// this compiler report
29 | /// - Parameter commandDesc: The command description
30 | func hasCompilerFlag(commandDesc: String) -> Bool
31 |
32 | /// Parses the Set of commands to look for swift compiler time outputs of type `SwiftcOption`
33 | /// - Parameter commands: Dictionary of command descriptions and ocurrences
34 | /// - Returns: A dictionary using the key as file and the Compiler time output as value
35 | func parse(from commands: [String: Int]) -> [String: [SwiftcOption]]
36 |
37 | }
38 |
39 | extension SwiftCompilerTimeOptionParser {
40 |
41 | /// Parses /users/mnf/project/SomeFile.swift:10:12
42 | /// - Returns: ("file:///users/mnf/project/SomeFile.swift", 10, 12)
43 | // swiftlint:disable:next large_tuple
44 | func parseNameAndLocation(from fileAndLocation: String) -> (String, Int, Int)? {
45 | // /users/mnf/project/SomeFile.swift:10:12
46 | let fileAndLocationParts = fileAndLocation.components(separatedBy: ":")
47 | let rawFile = fileAndLocationParts[0]
48 |
49 | guard rawFile != "" else {
50 | return nil
51 | }
52 |
53 | guard
54 | fileAndLocationParts.count == 3,
55 | let line = Int(fileAndLocationParts[1]),
56 | let column = Int(fileAndLocationParts[2])
57 | else {
58 | return nil
59 | }
60 |
61 | let file = prefixWithFileURL(fileName: rawFile)
62 |
63 | return (file, line, column)
64 | }
65 |
66 | /// Parses
67 | func parseCompileDuration(_ durationString: String) -> Double {
68 | if let duration = Double(durationString.replacingOccurrences(of: "ms", with: "")) {
69 | return duration
70 | }
71 | return 0.0
72 | }
73 |
74 | /// Transforms the fileName to a file URL to match the one in IDELogSection.documentURL
75 | /// It doesn't use `URL` class to do it, because it was slow in benchmarks
76 | /// - Parameter fileName: String with a fileName
77 | /// - Returns: A String with the URL to the file like `file:///`
78 | func prefixWithFileURL(fileName: String) -> String {
79 | return "file://\(fileName)"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/SwiftCompilerTypeCheckOptionParser.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Parses Swift Function times generated by `swiftc`
23 | /// if you pass the flags `-Xfrontend -debug-time-expression-type-checking`
24 | class SwiftCompilerTypeCheckOptionParser: SwiftCompilerTimeOptionParser {
25 |
26 | private static let compilerFlag = "-debug-time-expression-type-checking"
27 |
28 | func hasCompilerFlag(commandDesc: String) -> Bool {
29 | commandDesc.range(of: Self.compilerFlag) != nil
30 | }
31 |
32 | func parse(from commands: [String: Int]) -> [String: [SwiftTypeCheck]] {
33 | return commands.compactMap { parse(command: $0.key, occurrences: $0.value) }
34 | .joined().reduce([:]) { (typeChecksPerFile, typeCheckTime)
35 | -> [String: [SwiftTypeCheck]] in
36 | var typeChecksPerFile = typeChecksPerFile
37 | if var typeChecks = typeChecksPerFile[typeCheckTime.file] {
38 | typeChecks.append(typeCheckTime)
39 | typeChecksPerFile[typeCheckTime.file] = typeChecks
40 | } else {
41 | typeChecksPerFile[typeCheckTime.file] = [typeCheckTime]
42 | }
43 | return typeChecksPerFile
44 | }
45 | }
46 |
47 | private func parse(command: String, occurrences: Int) -> [SwiftTypeCheck]? {
48 | return command.components(separatedBy: "\r").compactMap { commandLine in
49 | // 0.14ms /users/mnf/project/SomeFile.swift:10:12
50 | let parts = commandLine.components(separatedBy: "\t")
51 |
52 | guard parts.count == 2 else {
53 | return nil
54 | }
55 |
56 | // 0.14ms
57 | let duration = parseCompileDuration(parts[0])
58 |
59 | // /users/mnf/project/SomeFile.swift:10:12
60 | let fileAndLocation = parts[1]
61 | guard let (file, line, column) = parseNameAndLocation(from: fileAndLocation) else {
62 | return nil
63 | }
64 |
65 | return SwiftTypeCheck(file: file,
66 | durationMS: duration,
67 | startingLine: line,
68 | startingColumn: column,
69 | occurrences: occurrences)
70 | }
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/SwiftFunctionTime.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Represents the time it took to the Swift Compiler to compile a function
23 | public struct SwiftFunctionTime: Encodable {
24 | /// URL of the file where the function is
25 | public let file: String
26 |
27 | /// Duration in Miliseconds
28 | public let durationMS: Double
29 |
30 | /// Line number where the function is declared
31 | public let startingLine: Int
32 |
33 | /// Column number where the function is declared
34 | public let startingColumn: Int
35 |
36 | /// function signature
37 | public let signature: String
38 |
39 | /// Number of occurences this function is compiled during the build
40 | public let occurrences: Int
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/parser/SwiftTypeCheck.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Represents the time it took to the Swift Compiler to type check an expression
23 | public struct SwiftTypeCheck: Encodable {
24 |
25 | /// URL of the file where the function is
26 | public let file: String
27 |
28 | /// Duration in Miliseconds
29 | public let durationMS: Double
30 |
31 | /// Line number where the function is declared
32 | public let startingLine: Int
33 |
34 | /// Column number where the function is declared
35 | public let startingColumn: Int
36 |
37 | /// Number of occurences this type is checked during the build
38 | public let occurrences: Int
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/ChromeTracerReporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | /// Creates a JSON document with the format used by Chrome Tracer.
23 | /// It can be visualized with the chrome://tracing tool
24 | public struct ChromeTracerReporter: LogReporter {
25 |
26 | private let microSecondsPerSecond: Double = 1_000_000
27 |
28 | public init() {}
29 |
30 | public func report(build: Any, output: ReporterOutput, rootOutput: String) throws {
31 | guard let steps = build as? BuildStep else {
32 | throw XCLogParserError.errorCreatingReport("Type not supported \(type(of: build))")
33 | }
34 | let events = toTraceEvents(rootStep: steps)
35 | let encoder = JSONEncoder()
36 | encoder.outputFormatting = .prettyPrinted
37 | let data = try encoder.encode(events)
38 | try output.write(report: data)
39 | }
40 |
41 | public func toTraceEvents(rootStep: BuildStep) -> [TrackEvent] {
42 | return rootStep.subSteps.enumerated().flatMap { (index, target) -> [TrackEvent] in
43 | return targetToTraceEvent(target: target, index: index)
44 | }
45 | }
46 |
47 | private func targetToTraceEvent(target: BuildStep, index: Int) -> [TrackEvent] {
48 | let threadEvent = toThreadEvent(target, index: index)
49 | let targetEvents = toTargetEvents(target, index: index)
50 | let stepsEvents = target.subSteps.map { step -> TrackEvent in
51 | return toTrackEvent(step, target: target, index: index)
52 | }
53 | return [threadEvent] + targetEvents + stepsEvents
54 | }
55 |
56 | private func toThreadEvent(_ target: BuildStep, index: Int) -> TrackEvent {
57 | let event = TrackEvent(name: TrackEvent.threadEventName, ph: .thread, pid: 1, tid: index)
58 | event.args["name"] = target.title
59 | return event
60 | }
61 |
62 | private func toTargetEvents(_ target: BuildStep, index: Int) -> [TrackEvent] {
63 | let startEvent = TrackEvent(name: target.title,
64 | ph: .durationStart,
65 | pid: 1,
66 | tid: index,
67 | cat: target.title,
68 | ts: target.startTimestamp * microSecondsPerSecond,
69 | id: index)
70 |
71 | let endEvent = TrackEvent(name: target.title,
72 | ph: .durationEnd,
73 | pid: 1,
74 | tid: index,
75 | cat: target.title,
76 | ts: target.endTimestamp * microSecondsPerSecond,
77 | id: index)
78 | return [startEvent, endEvent]
79 | }
80 |
81 | private func toTrackEvent(_ step: BuildStep, target: BuildStep, index: Int) -> TrackEvent {
82 | let event = TrackEvent(name: step.title,
83 | ph: .complete,
84 | pid: 1,
85 | tid: index,
86 | cat: target.title,
87 | ts: step.startTimestamp * microSecondsPerSecond,
88 | id: index,
89 | dur: step.duration * microSecondsPerSecond)
90 | event.args["signature"] = step.signature
91 | event.args["type"] = step.detailStepType.rawValue
92 | return event
93 | }
94 |
95 | }
96 |
97 | public enum TrackPhase: String {
98 | case thread = "M"
99 | case durationStart = "B"
100 | case durationEnd = "E"
101 | case complete = "X"
102 | }
103 | // swiftlint:disable identifier_name
104 | /// The trace Event format used by chrome.
105 | /// [Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU)
106 | public class TrackEvent: Encodable {
107 |
108 | static let threadEventName = "thread_name"
109 |
110 | let name: String
111 |
112 | let ph: TrackPhase
113 | let pid: Int
114 | let tid: Int
115 | let cat: String
116 | let ts: Double
117 | let id: Int
118 | let dur: Double
119 | var args: [String: String] = [String: String]()
120 |
121 | public init(name: String, ph: TrackPhase, pid: Int, tid: Int, cat: String, ts: Double, id: Int, dur: Double) {
122 | self.name = name
123 | self.ph = ph
124 | self.pid = pid
125 | self.tid = tid
126 | self.cat = cat
127 | self.ts = ts
128 | self.id = id
129 | self.dur = dur
130 | }
131 |
132 | public convenience init(name: String, ph: TrackPhase, pid: Int, tid: Int, cat: String, ts: Double, id: Int) {
133 | self.init(name: name, ph: ph, pid: pid, tid: tid, cat: cat, ts: ts, id: id, dur: 0.0)
134 | }
135 |
136 | public convenience init(name: String, ph: TrackPhase, pid: Int, tid: Int) {
137 | self.init(name: name, ph: ph, pid: pid, tid: tid, cat: "", ts: 0.0, id: 0, dur: 0.0)
138 | }
139 |
140 | private enum CodingKeys: String, CodingKey {
141 | case name
142 | case ph
143 | case pid
144 | case tid
145 | case args
146 | case cat
147 | case ts
148 | case id
149 | case dur
150 | }
151 |
152 | public func encode(to encoder: Encoder) throws {
153 | var container = encoder.container(keyedBy: CodingKeys.self)
154 | try container.encode(name, forKey: .name)
155 | try container.encode(ph.rawValue, forKey: .ph)
156 | try container.encode(pid, forKey: .pid)
157 | try container.encode(tid, forKey: .tid)
158 | if !args.isEmpty {
159 | try container.encode(args, forKey: .args)
160 | }
161 | if ph != .thread {
162 | try container.encode(cat, forKey: .cat)
163 | try container.encode(ts, forKey: .ts)
164 | try container.encode(id, forKey: .id)
165 | }
166 | if ph == .complete {
167 | try container.encode(dur, forKey: .dur)
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/FlatJsonReporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct FlatJsonReporter: LogReporter {
23 |
24 | public init() {}
25 |
26 | public func report(build: Any, output: ReporterOutput, rootOutput: String) throws {
27 | guard let steps = build as? BuildStep else {
28 | throw XCLogParserError.errorCreatingReport("Type not supported \(type(of: build))")
29 | }
30 | let encoder = JSONEncoder()
31 | encoder.outputFormatting = .prettyPrinted
32 | let json = try encoder.encode(steps.flatten())
33 | try output.write(report: json)
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/IssuesReporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License .
19 |
20 | import Foundation
21 |
22 | struct Issues: Codable {
23 | let warnings: [Notice]
24 | let errors: [Notice]
25 | }
26 |
27 | public struct IssuesReporter: LogReporter {
28 |
29 | public init() {}
30 |
31 | public func report(build: Any, output: ReporterOutput, rootOutput: String) throws {
32 | guard let steps = build as? BuildStep else {
33 | throw XCLogParserError.errorCreatingReport("Type not supported \(type(of: build))")
34 | }
35 | let encoder = JSONEncoder()
36 | encoder.outputFormatting = .prettyPrinted
37 | let json = try encoder.encode(getIssues(from: steps))
38 | try output.write(report: json)
39 | }
40 |
41 | }
42 |
43 | private func getIssues(from step: BuildStep) -> Issues {
44 | return Issues(warnings: getIssues(from: step, keyPath: \.warnings),
45 | errors: getIssues(from: step, keyPath: \.errors))
46 | }
47 |
48 | private func getIssues(from step: BuildStep, keyPath: KeyPath) -> [Notice] {
49 | return (step[keyPath: keyPath] ?? [])
50 | + step.subSteps.flatMap { getIssues(from: $0, keyPath: keyPath )}
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/JsonReporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct JsonReporter: LogReporter {
23 |
24 | public init() {}
25 |
26 | public func report(build: Any, output: ReporterOutput, rootOutput: String) throws {
27 | switch build {
28 | case let steps as BuildStep:
29 | try report(encodable: steps, output: output)
30 | case let logEntries as [LogManifestEntry]:
31 | try report(encodable: logEntries, output: output)
32 | case let activityLog as IDEActivityLog:
33 | try report(encodable: activityLog, output: output)
34 | default:
35 | throw XCLogParserError.errorCreatingReport("Type not supported \(type(of: build))")
36 | }
37 | }
38 |
39 | private func report(encodable: T, output: ReporterOutput) throws {
40 | let encoder = JSONEncoder()
41 | encoder.outputFormatting = .prettyPrinted
42 | let json = try encoder.encode(encodable)
43 | try output.write(report: json)
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/LogReporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public protocol LogReporter {
23 |
24 | func report(build: Any, output: ReporterOutput, rootOutput: String) throws
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/Reporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public enum Reporter: String {
23 | case json
24 | case flatJson
25 | case summaryJson
26 | case chromeTracer
27 | case html
28 | case issues
29 |
30 | public func makeLogReporter() -> LogReporter {
31 | switch self {
32 | case .chromeTracer:
33 | return ChromeTracerReporter()
34 | case .json:
35 | return JsonReporter()
36 | case .flatJson:
37 | return FlatJsonReporter()
38 | case .summaryJson:
39 | return SummaryJsonReporter()
40 | case .html:
41 | return HtmlReporter()
42 | case .issues:
43 | return IssuesReporter()
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/XCLogParser/reporter/SummaryJsonReporter.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | public struct SummaryJsonReporter: LogReporter {
23 |
24 | public init() {}
25 |
26 | public func report(build: Any, output: ReporterOutput, rootOutput: String) throws {
27 | guard let steps = build as? BuildStep else {
28 | throw XCLogParserError.errorCreatingReport("Type not supported \(type(of: build))")
29 | }
30 | let encoder = JSONEncoder()
31 | encoder.outputFormatting = .prettyPrinted
32 | let json = try encoder.encode(steps.summarize())
33 | try output.write(report: json)
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/DumpCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import ArgumentParser
21 | import XCLogParser
22 |
23 | struct DumpCommand: ParsableCommand {
24 |
25 | static let configuration = CommandConfiguration(
26 | commandName: "dump",
27 | abstract: "Dumps the xcactivitylog file into a JSON document"
28 | )
29 |
30 | @Option(name: .long, help: "The path to a .xcactivitylog file.")
31 | var file: String?
32 |
33 | @Option(name: .customLong("derived_data"),
34 | help: """
35 | The path to the DerivedData directory.
36 | Use it if it's not the default ~/Library/Developer/Xcode/DerivedData/.
37 | """)
38 | var derivedData: String?
39 |
40 | @Option(name: .long,
41 | help: """
42 | The name of an Xcode project. The tool will try to find the latest log folder
43 | with this prefix in the DerivedData directory. Use with `--strictProjectName`
44 | for stricter name matching.
45 | """)
46 | var project: String?
47 |
48 | @Option(name: .long,
49 | help: """
50 | The path to the .xcworkspace folder. Used to find the Derived Data project directory
51 | if no `--project` flag is present.
52 | """)
53 | var workspace: String?
54 |
55 | @Option(name: .long,
56 | help: """
57 | The path to the .xcodeproj folder. Used to find the Derived Data project directory
58 | if no `--project` and no `--workspace` flag is present.
59 | """)
60 | var xcodeproj: String?
61 |
62 | @Flag(help: """
63 | Redacts the username of the paths found in the log.
64 | For instance, /Users/timcook/project will be /Users//project
65 | """)
66 | var redacted: Bool = false
67 |
68 | @Flag(name: .customLong("without_build_specific_information"),
69 | help: """
70 | Removes build specific information from the logs.
71 | For instance, DerivedData/Product-bolnckhlbzxpxoeyfujluasoupft/Build
72 | will be DerivedData/Product/Build
73 | """)
74 | var withoutBuildSpecificInformation: Bool = false
75 |
76 | @Flag(name: .customLong("strictProjectName"),
77 | help: """
78 | Use strict name testing when trying to find the latest version of the project
79 | in the DerivedData directory.
80 | """)
81 | var strictProjectName: Bool = false
82 |
83 | @Option(name: .long,
84 | help: """
85 | Optional. Path to which the report will be written to. If not specified,
86 | the report will be written to the standard output.
87 | """)
88 | var output: String?
89 |
90 | mutating func validate() throws {
91 | if !hasValidLogOptions() {
92 | throw ValidationError("""
93 | Please, provide a way to locate the .xcactivity log of your project.
94 | You can use --file or --project or --workspace or --xcodeproj.
95 | Type `xclogparser help dump` to get more information.`
96 | """)
97 | }
98 | }
99 |
100 | func run() throws {
101 | let commandHandler = CommandHandler()
102 | let logOptions = LogOptions(projectName: project ?? "",
103 | xcworkspacePath: workspace ?? "",
104 | xcodeprojPath: xcodeproj ?? "",
105 | derivedDataPath: derivedData ?? "",
106 | xcactivitylogPath: file ?? "",
107 | strictProjectName: strictProjectName)
108 | let actionOptions = ActionOptions(reporter: .json,
109 | outputPath: output ?? "",
110 | redacted: redacted,
111 | withoutBuildSpecificInformation:
112 | withoutBuildSpecificInformation
113 | )
114 | let action = Action.dump(options: actionOptions)
115 | let command = Command(logOptions: logOptions, action: action)
116 |
117 | try commandHandler.handle(command: command)
118 | }
119 |
120 | private func hasValidLogOptions() -> Bool {
121 | return !file.isBlank || !project.isBlank || !workspace.isBlank || !xcodeproj.isBlank
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/MainCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import ArgumentParser
21 |
22 | struct MainCommand: ParsableCommand {
23 | static let configuration = CommandConfiguration(commandName: "xclogparser",
24 | subcommands: [
25 | ParseCommand.self,
26 | DumpCommand.self,
27 | ManifestCommand.self,
28 | VersionCommand.self
29 | ])
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/ManifestCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import ArgumentParser
21 | import XCLogParser
22 |
23 | struct ManifestCommand: ParsableCommand {
24 |
25 | static let configuration = CommandConfiguration(
26 | commandName: "manifest",
27 | abstract: "Shows the content of a LogManifest plist file as a JSON document."
28 | )
29 |
30 | @Option(name: .customLong("long_manifest"), help: "The path to an existing LogStoreManifest.plist.")
31 | var logManifest: String?
32 |
33 | @Option(name: .customLong("derived_data"),
34 | help: """
35 | The path to the DerivedData directory.
36 | Use it if it's not the default ~/Library/Developer/Xcode/DerivedData/.
37 | """)
38 | var derivedData: String?
39 |
40 | @Option(name: .long,
41 | help: """
42 | The name of an Xcode project. The tool will try to find the latest log folder
43 | with this prefix in the DerivedData directory. Use with `--strictProjectName`
44 | for stricter name matching.
45 | """)
46 | var project: String?
47 |
48 | @Option(name: .long,
49 | help: """
50 | The path to the .xcworkspace folder. Used to find the Derived Data project directory
51 | if no `--project` flag is present.
52 | """)
53 | var workspace: String?
54 |
55 | @Option(name: .long,
56 | help: """
57 | The path to the .xcodeproj folder. Used to find the Derived Data project directory
58 | if no `--project` and no `--workspace` flag is present.
59 | """)
60 | var xcodeproj: String?
61 |
62 | @Flag(name: .customLong("strictProjectName"),
63 | help: """
64 | Use strict name testing when trying to find the latest version of the project
65 | in the DerivedData directory.
66 | """)
67 | var strictProjectName: Bool = false
68 |
69 | @Option(name: .long,
70 | help: """
71 | Optional. Path to which the report will be written to. If not specified,
72 | the report will be written to the standard output.
73 | """)
74 | var output: String?
75 |
76 | mutating func validate() throws {
77 | if !hasValidLogOptions() {
78 | throw ValidationError("""
79 | Please, provide a way to locate the .xcactivity log of your project.
80 | You can use --file or --project or --workspace or --xcodeproj.
81 | Type `xclogparser help manifest` to get more information.`
82 | """)
83 | }
84 | }
85 |
86 | func run() throws {
87 | let commandHandler = CommandHandler()
88 | let logOptions = LogOptions(projectName: project ?? "",
89 | xcworkspacePath: workspace ?? "",
90 | xcodeprojPath: xcodeproj ?? "",
91 | derivedDataPath: derivedData ?? "",
92 | logManifestPath: logManifest ?? "",
93 | strictProjectName: strictProjectName)
94 |
95 | let actionOptions = ActionOptions(reporter: .json,
96 | outputPath: output ?? "",
97 | redacted: false,
98 | withoutBuildSpecificInformation: false)
99 | let action = Action.manifest(options: actionOptions)
100 | let command = Command(logOptions: logOptions, action: action)
101 |
102 | try commandHandler.handle(command: command)
103 | }
104 |
105 | private func hasValidLogOptions() -> Bool {
106 | return !logManifest.isBlank || !project.isBlank || !workspace.isBlank || !xcodeproj.isBlank
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/Optional+Blank.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension Optional where Wrapped == String {
23 |
24 | var isBlank: Bool {
25 | return self?.isBlank ?? true
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/ParseCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import ArgumentParser
21 | import XCLogParser
22 |
23 | struct ParseCommand: ParsableCommand {
24 |
25 | static let configuration = CommandConfiguration(
26 | commandName: "parse",
27 | abstract: "Parses the content of an xcactivitylog file"
28 | )
29 |
30 | @Option(name: .long, help: "The path to a .xcactivitylog file.")
31 | var file: String?
32 |
33 | @Option(name: .customLong("derived_data"),
34 | help: """
35 | The path to the DerivedData directory.
36 | Use it if it's not the default ~/Library/Developer/Xcode/DerivedData/.
37 | """)
38 | var derivedData: String?
39 |
40 | @Option(name: .long,
41 | help: """
42 | The name of an Xcode project. The tool will try to find the latest log folder
43 | with this prefix in the DerivedData directory. Use with `--strictProjectName`
44 | for stricter name matching.
45 | """)
46 | var project: String?
47 |
48 | @Option(name: .long,
49 | help: """
50 | The path to the .xcworkspace folder. Used to find the Derived Data project directory
51 | if no `--project` flag is present.
52 | """)
53 | var workspace: String?
54 |
55 | @Option(name: .long,
56 | help: """
57 | The path to the .xcodeproj folder. Used to find the Derived Data project directory
58 | if no `--project` and no `--workspace` flag is present.
59 | """)
60 | var xcodeproj: String?
61 |
62 | @Option(name: .long,
63 | help: """
64 | Mandatory. The reporter to use. It could be `json`, `flatJson`, `summaryJson`,
65 | `chromeTracer`, `html` or `btr`
66 | """)
67 | var reporter: String?
68 |
69 | @Option(name: .customLong("machine_name"),
70 | help: """
71 | Optional. The name of the machine. If not specified, the host name will be used.
72 | """)
73 | var machineName: String?
74 |
75 | @Flag(help: """
76 | Redacts the username of the paths found in the log.
77 | For instance, /Users/timcook/project will be /Users//project
78 | """)
79 | var redacted: Bool = false
80 |
81 | @Flag(name: .customLong("without_build_specific_information"),
82 | help: """
83 | Removes build specific information from the logs.
84 | For instance, DerivedData/Product-bolnckhlbzxpxoeyfujluasoupft/Build
85 | will be DerivedData/Product/Build
86 | """)
87 | var withoutBuildSpecificInformation: Bool = false
88 |
89 | @Flag(name: .customLong("strictProjectName"),
90 | help: """
91 | Use strict name testing when trying to find the latest version of the project
92 | in the DerivedData directory.
93 | """)
94 | var strictProjectName: Bool = false
95 |
96 | @Option(name: .long,
97 | help: """
98 | Optional. Path to which the report will be written to. If not specified,
99 | the report will be written to the standard output.
100 | """)
101 | var output: String?
102 |
103 | @Option(name: .customLong("rootOutput"),
104 | help: """
105 | Optional. Add the project output into the given current path,
106 | example: myGivenPath/report.json.
107 | """)
108 | var rootOutput: String?
109 |
110 | @Flag(name: .customLong("omit_warnings"),
111 | help: """
112 | If present, the report will omit the Warnings found in the log.
113 | Useful to reduce the size of the final report.
114 | """)
115 | var omitWarnings: Bool = false
116 |
117 | @Flag(name: .customLong("omit_notes"),
118 | help: """
119 | If present, the report will omit the Notes found in the log.
120 | Useful to reduce the size of the final report.
121 | """)
122 | var omitNotes: Bool = false
123 |
124 | @Flag(name: .customLong("trunc_large_issues"),
125 | help: """
126 | If present, for tasks with more than a 100 issues (warnings, notes or errors)
127 | Those will be truncated to a 100.
128 | Useful to reduce the amount of memory used and the size of the report.
129 | """)
130 | var truncLargeIssues: Bool = false
131 |
132 | mutating func validate() throws {
133 | if !hasValidLogOptions() {
134 | throw ValidationError("""
135 | Please, provide a way to locate the .xcactivity log of your project.
136 | You can use --file or --project or --workspace or --xcodeproj.
137 | Type `xclogparser help parse` to get more information.`
138 | """)
139 | }
140 | guard let reporter = reporter, reporter.isEmpty == false else {
141 | throw ValidationError("""
142 | You need to specify a reporter. Type `xclogparser help parse` to see the available ones.
143 | """)
144 | }
145 | guard Reporter(rawValue: reporter) != nil else {
146 | throw ValidationError("""
147 | \(reporter) is not a valid reporter. Please provide a valid reporter to use.
148 | Type `xclogparser help parse` to see the available ones.
149 | """)
150 | }
151 | }
152 |
153 | func run() throws {
154 | guard let reporter = reporter else {
155 | return
156 | }
157 | guard let xclReporter = Reporter(rawValue: reporter) else {
158 | return
159 | }
160 | let commandHandler = CommandHandler()
161 | let logOptions = LogOptions(projectName: project ?? "",
162 | xcworkspacePath: workspace ?? "",
163 | xcodeprojPath: xcodeproj ?? "",
164 | derivedDataPath: derivedData ?? "",
165 | xcactivitylogPath: file ?? "",
166 | strictProjectName: strictProjectName)
167 | let actionOptions = ActionOptions(reporter: xclReporter,
168 | outputPath: output ?? "",
169 | redacted: redacted,
170 | withoutBuildSpecificInformation: withoutBuildSpecificInformation,
171 | machineName: machineName,
172 | rootOutput: rootOutput ?? "",
173 | omitWarningsDetails: omitWarnings,
174 | omitNotesDetails: omitNotes,
175 | truncLargeIssues: truncLargeIssues)
176 | let action = Action.parse(options: actionOptions)
177 | let command = Command(logOptions: logOptions, action: action)
178 | try commandHandler.handle(command: command)
179 | }
180 |
181 | private func hasValidLogOptions() -> Bool {
182 | return !file.isBlank || !project.isBlank || !workspace.isBlank || !xcodeproj.isBlank
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/String+Blank.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | extension String {
23 |
24 | var isBlank: Bool {
25 | return allSatisfy({ $0.isWhitespace })
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/commands/VersionCommand.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import ArgumentParser
21 | import XCLogParser
22 |
23 | struct VersionCommand: ParsableCommand {
24 |
25 | static let configuration = CommandConfiguration(
26 | commandName: "version",
27 | abstract: "Displays the version of the tool."
28 | )
29 |
30 | func run() {
31 | print("XCLogParser \(Version.current)")
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/XCLogParserApp/main.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | MainCommand.main()
23 |
--------------------------------------------------------------------------------
/Sources/XcodeHasher/XcodeHasher.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 | import CryptoSwift
22 |
23 | // Thanks to https://pewpewthespells.com/blog/xcode_deriveddata_hashes.html for
24 | // the initial Objective-C implementation.
25 | public class XcodeHasher {
26 |
27 | enum HashingError: Error {
28 | case invalidPartitioning
29 | }
30 |
31 | public static func hashString(for path: String) throws -> String {
32 | // Initialize a 28 `String` array since we can't initialize empty `Character`s.
33 | var result = Array(repeating: "", count: 28)
34 |
35 | // Compute md5 hash of the path
36 | let digest = path.bytes.md5()
37 |
38 | // Split 16 bytes into two chunks of 8 bytes each.
39 | let partitions = stride(from: 0, to: digest.count, by: 8).map {
40 | Array(digest[$0..=5.0)
47 | var startValue = UInt64(bigEndian: Data(firstHalf).withUnsafeBytes { $0.load(as: UInt64.self) })
48 | #else
49 | var startValue = UInt64(bigEndian: Data(firstHalf).withUnsafeBytes { $0.pointee })
50 | #endif
51 | for index in stride(from: 13, through: 0, by: -1) {
52 | // Take the startValue % 26 to restrict to alphabetic characters and add 'a' scalar value (97).
53 | let char = String(UnicodeScalar(Int(startValue % 26) + 97)!)
54 | result[index] = char
55 | startValue /= 26
56 | }
57 | // We would need to reverse the bytes, so we just read them in big endian.
58 | #if swift(>=5.0)
59 | startValue = UInt64(bigEndian: Data(secondHalf).withUnsafeBytes { $0.load(as: UInt64.self) })
60 | #else
61 | startValue = UInt64(bigEndian: Data(secondHalf).withUnsafeBytes { $0.pointee })
62 | #endif
63 | for index in stride(from: 27, through: 14, by: -1) {
64 | // Take the startValue % 26 to restrict to alphabetic characters and add 'a' scalar value (97).
65 | let char = String(UnicodeScalar(Int(startValue % 26) + 97)!)
66 | result[index] = char
67 | startValue /= 26
68 | }
69 |
70 | return result.joined()
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import XCLogParserTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += XCLogParserTests.__allTests()
7 |
8 | XCTMain(tests)
9 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/BuildStatusSanitizerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import XCLogParser
3 |
4 | final class BuildStatusSanitizerTests: XCTestCase {
5 | private let sut = BuildStatusSanitizer.self
6 | private let succeededStatus = "succeeded"
7 | private let failedStatus = "failed"
8 | private let stoppedStatus = "stopped"
9 |
10 | func testSanitizeWhenStringIsOnlySucceededThenReturnSucceeded() {
11 | let sanitizedStatus = sut.sanitize(originalStatus: succeededStatus)
12 |
13 | XCTAssertEqual(sanitizedStatus, succeededStatus)
14 | }
15 |
16 | func testSanitizeWhenStringIsOnlyFailedThenReturnFailed() {
17 | let sanitizedStatus = sut.sanitize(originalStatus: failedStatus)
18 |
19 | XCTAssertEqual(sanitizedStatus, failedStatus)
20 | }
21 |
22 | func testSanitizeWhenStringIsOnlyStoppedThenReturnStopped() {
23 | let sanitizedStatus = sut.sanitize(originalStatus: stoppedStatus)
24 |
25 | XCTAssertEqual(sanitizedStatus, stoppedStatus)
26 | }
27 |
28 | func testSanitizeWhenStringIsSucceededAndContainsCleanThenReturnSucceeded() {
29 | let sanitizedStatus = sut.sanitize(originalStatus: "Clean " + succeededStatus)
30 |
31 | XCTAssertEqual(sanitizedStatus, succeededStatus)
32 | }
33 |
34 | func testSanitizeWhenStringIsFailedAndContainsCleanThenReturnFailed() {
35 | let sanitizedStatus = sut.sanitize(originalStatus: "Clean " + failedStatus)
36 |
37 | XCTAssertEqual(sanitizedStatus, failedStatus)
38 | }
39 |
40 | func testSanitizeWhenStringIsStoppedAndContainsCleanThenReturnStopped() {
41 | let sanitizedStatus = sut.sanitize(originalStatus: "Clean " + stoppedStatus)
42 |
43 | XCTAssertEqual(sanitizedStatus, stoppedStatus)
44 | }
45 |
46 | func testSanitizeWhenStringIsSucceededAndContainsBuildThenReturnSucceeded() {
47 | let sanitizedStatus = sut.sanitize(originalStatus: "Build " + succeededStatus)
48 |
49 | XCTAssertEqual(sanitizedStatus, succeededStatus)
50 | }
51 |
52 | func testSanitizeWhenStringIsFailedAndContainsBuildThenReturnFailed() {
53 | let sanitizedStatus = sut.sanitize(originalStatus: "Build " + failedStatus)
54 |
55 | XCTAssertEqual(sanitizedStatus, failedStatus)
56 | }
57 |
58 | func testSanitizeWhenStringIsStoppedAndContainsBuildThenReturnStopped() {
59 | let sanitizedStatus = sut.sanitize(originalStatus: "Build " + stoppedStatus)
60 |
61 | XCTAssertEqual(sanitizedStatus, stoppedStatus)
62 | }
63 |
64 | func testSanitizeWhenStringIsSucceededAndContainsSurroundingSpacesThenReturnSucceededTrimmed() {
65 | let sanitizedStatus = sut.sanitize(originalStatus: " " + succeededStatus + " ")
66 |
67 | XCTAssertEqual(sanitizedStatus, succeededStatus)
68 | }
69 |
70 | func testSanitizeWhenStringIsFailedAndContainsSurroundingSpacesThenReturnFailedTrimmed() {
71 | let sanitizedStatus = sut.sanitize(originalStatus: " " + failedStatus + " ")
72 |
73 | XCTAssertEqual(sanitizedStatus, failedStatus)
74 | }
75 |
76 | func testSanitizeWhenStringIsStoppedAndContainsSurroundingSpacesThenReturnStoppedTrimmed() {
77 | let sanitizedStatus = sut.sanitize(originalStatus: " " + stoppedStatus + " ")
78 |
79 | XCTAssertEqual(sanitizedStatus, stoppedStatus)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/BuildStep+TestUtils.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import XCLogParser
3 |
4 | func makeFakeBuildStep(title: String,
5 | type: BuildStepType,
6 | detailStepType: DetailStepType,
7 | startTimestamp: Double,
8 | fetchedFromCache: Bool) -> BuildStep {
9 | return BuildStep(type: type,
10 | machineName: "",
11 | buildIdentifier: "",
12 | identifier: "",
13 | parentIdentifier: "",
14 | domain: "",
15 | title: title,
16 | signature: "",
17 | startDate: "",
18 | endDate: "",
19 | startTimestamp: startTimestamp,
20 | endTimestamp: 0,
21 | duration: 0,
22 | detailStepType: detailStepType,
23 | buildStatus: "",
24 | schema: "",
25 | subSteps: [],
26 | warningCount: 0,
27 | errorCount: 0,
28 | architecture: "",
29 | documentURL: "",
30 | warnings: nil,
31 | errors: nil,
32 | notes: nil,
33 | swiftFunctionTimes: nil,
34 | fetchedFromCache: fetchedFromCache,
35 | compilationEndTimestamp: 0,
36 | compilationDuration: 0,
37 | clangTimeTraceFile: nil,
38 | linkerStatistics: nil,
39 | swiftTypeCheckTimes: nil)
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/ChromeTracerOutputTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import XCTest
21 | @testable import XCLogParser
22 |
23 | class ChromeTracerOutputTests: XCTestCase {
24 |
25 | let output = ChromeTracerReporter()
26 |
27 | func testTargetToTraceEvent() {
28 | let root = getBuildStep()
29 | let result = output.toTraceEvents(rootStep: root)
30 | XCTAssertEqual(6, result.count)
31 | guard let threadEvent = result.first else {
32 | return
33 | }
34 | XCTAssertEqual("thread_name", threadEvent.name)
35 | XCTAssertEqual("M", threadEvent.ph.rawValue)
36 | XCTAssertEqual(1, threadEvent.pid)
37 | XCTAssertEqual(0, threadEvent.tid)
38 |
39 | let startTargetEvent = result[1]
40 | XCTAssertEqual("MyTarget", startTargetEvent.name)
41 | XCTAssertEqual("B", startTargetEvent.ph.rawValue)
42 | XCTAssertEqual(1, threadEvent.pid)
43 | XCTAssertEqual(0, threadEvent.tid)
44 |
45 | let endTargetEvent = result[2]
46 | XCTAssertEqual("MyTarget", endTargetEvent.name)
47 | XCTAssertEqual("E", endTargetEvent.ph.rawValue)
48 | XCTAssertEqual(1, endTargetEvent.pid)
49 | XCTAssertEqual(0, endTargetEvent.tid)
50 |
51 | }
52 |
53 | private func getBuildStep() -> BuildStep {
54 | let start = Date()
55 | let end = start.addingTimeInterval(100 * 100)
56 | let root = BuildStep(type: .main,
57 | machineName: "",
58 | buildIdentifier: "ABC",
59 | identifier: "ABC1",
60 | parentIdentifier: "",
61 | domain: "",
62 | title: "MyApp",
63 | signature: "Build MyApp",
64 | startDate: "",
65 | endDate: "",
66 | startTimestamp: start.timeIntervalSince1970,
67 | endTimestamp: end.timeIntervalSince1970,
68 | duration: 100 * 100,
69 | detailStepType: .none,
70 | buildStatus: "Build succeeded",
71 | schema: "MyApp",
72 | subSteps: getTargets(start: start),
73 | warningCount: 0,
74 | errorCount: 0,
75 | architecture: "",
76 | documentURL: "",
77 | warnings: nil,
78 | errors: nil,
79 | notes: nil,
80 | swiftFunctionTimes: nil,
81 | fetchedFromCache: false,
82 | compilationEndTimestamp: end.timeIntervalSince1970,
83 | compilationDuration: 100 * 100,
84 | clangTimeTraceFile: nil,
85 | linkerStatistics: nil,
86 | swiftTypeCheckTimes: nil
87 | )
88 | return root
89 | }
90 |
91 | // swiftlint:disable function_body_length
92 | private func getTargets(start: Date) -> [BuildStep] {
93 | let end = start.addingTimeInterval(50 * 100)
94 | let target1 = BuildStep(type: .target,
95 | machineName: "",
96 | buildIdentifier: "ABC",
97 | identifier: "ABC1_1",
98 | parentIdentifier: "ABC1",
99 | domain: "",
100 | title: "MyTarget",
101 | signature: "Build MyTarget",
102 | startDate: "",
103 | endDate: "",
104 | startTimestamp: start.timeIntervalSince1970,
105 | endTimestamp: end.timeIntervalSince1970,
106 | duration: 50 * 100,
107 | detailStepType: .none,
108 | buildStatus: "Build succeeded",
109 | schema: "MyApp",
110 | subSteps: [BuildStep](),
111 | warningCount: 0,
112 | errorCount: 0,
113 | architecture: "",
114 | documentURL: "",
115 | warnings: nil,
116 | errors: nil,
117 | notes: nil,
118 | swiftFunctionTimes: nil,
119 | fetchedFromCache: false,
120 | compilationEndTimestamp: end.timeIntervalSince1970,
121 | compilationDuration: 50 * 100,
122 | clangTimeTraceFile: nil,
123 | linkerStatistics: nil,
124 | swiftTypeCheckTimes: nil
125 | )
126 |
127 | let end2 = end.addingTimeInterval(50 * 100)
128 | let target2 = BuildStep(type: .target,
129 | machineName: "",
130 | buildIdentifier: "ABC",
131 | identifier: "ABC1_2",
132 | parentIdentifier: "ABC1",
133 | domain: "",
134 | title: "MyTarget2",
135 | signature: "Build MyTarget2",
136 | startDate: "",
137 | endDate: "",
138 | startTimestamp: end.timeIntervalSince1970,
139 | endTimestamp: end2.timeIntervalSince1970,
140 | duration: 50 * 100,
141 | detailStepType: .none,
142 | buildStatus: "Build succeeded",
143 | schema: "MyApp",
144 | subSteps: [BuildStep](),
145 | warningCount: 0,
146 | errorCount: 0,
147 | architecture: "",
148 | documentURL: "",
149 | warnings: nil,
150 | errors: nil,
151 | notes: nil,
152 | swiftFunctionTimes: nil,
153 | fetchedFromCache: false,
154 | compilationEndTimestamp: end2.timeIntervalSince1970,
155 | compilationDuration: 50 * 100,
156 | clangTimeTraceFile: nil,
157 | linkerStatistics: nil,
158 | swiftTypeCheckTimes: nil
159 | )
160 | return [target1, target2]
161 |
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/ClangCompilerParserTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import XCTest
21 | @testable import XCLogParser
22 |
23 | class ClangCompilerParserTests: XCTestCase {
24 | let parser = ClangCompilerParser()
25 |
26 | func testParseTimeTraceFile() throws {
27 | let text = """
28 | Time trace json-file dumped to /Users/project/ObjectsDirectory/UIViewController+Utility.json\r\
29 | Use chrome://tracing or Speedscope App (https://www.speedscope.app) for flamegraph visualization\r
30 | """
31 | let clangCompileLogSection = getFakeClangSection(text: text, commandDescription: "-ftime-trace")
32 |
33 | let expectedFile = "/Users/project/ObjectsDirectory/UIViewController+Utility.json"
34 | let timeTraceFile = parser.parseTimeTraceFile(clangCompileLogSection)
35 | XCTAssertEqual(expectedFile, timeTraceFile)
36 | }
37 |
38 | func testParseLinkerStatistics() throws {
39 | let text = """
40 | ld total time: 561.6 milliseconds ( 100.0%)\r\
41 | option parsing time: 51.7 milliseconds ( 9.2%)\r\
42 | object file processing: 0.0 milliseconds ( 0.0%)\r\
43 | resolve symbols: 336.1 milliseconds ( 59.8%)\r\
44 | build atom list: 0.0 milliseconds ( 0.0%)\r\
45 | passess: 97.3 milliseconds ( 17.3%)\r\
46 | write output: 76.3 milliseconds ( 13.5%)\r\
47 | pageins=7464, pageouts=0, faults=31012\r\
48 | processed 5 object files, totaling 140,932 bytes\r\
49 | processed 42 archive files, totaling 24,362,016 bytes\r\
50 | processed 87 dylib files\r\
51 | wrote output file totaling 8,758,732 bytes\r
52 | """
53 | let clangCompileLogSection = getFakeClangSection(text: text, commandDescription: "-print_statistics")
54 | let statistics = parser.parseLinkerStatistics(clangCompileLogSection)!
55 | XCTAssertEqual(statistics.totalMS, 561.6, accuracy: 0.0001)
56 | XCTAssertEqual(statistics.optionParsingMS, 51.7, accuracy: 0.0001)
57 | XCTAssertEqual(statistics.optionParsingPercent, 9.2, accuracy: 0.0001)
58 | XCTAssertEqual(statistics.objectFileProcessingMS, 0.0, accuracy: 0.0001)
59 | XCTAssertEqual(statistics.objectFileProcessingPercent, 0.0, accuracy: 0.0001)
60 | XCTAssertEqual(statistics.resolveSymbolsMS, 336.1, accuracy: 0.0001)
61 | XCTAssertEqual(statistics.resolveSymbolsPercent, 59.8, accuracy: 0.0001)
62 | XCTAssertEqual(statistics.buildAtomListMS, 0.0, accuracy: 0.0001)
63 | XCTAssertEqual(statistics.buildAtomListPercent, 0.0, accuracy: 0.0001)
64 | XCTAssertEqual(statistics.runPassesMS, 97.3, accuracy: 0.0001)
65 | XCTAssertEqual(statistics.runPassesPercent, 17.3, accuracy: 0.0001)
66 | XCTAssertEqual(statistics.writeOutputMS, 76.3, accuracy: 0.0001)
67 | XCTAssertEqual(statistics.writeOutputPercent, 13.5, accuracy: 0.0001)
68 | XCTAssertEqual(statistics.pageins, 7464)
69 | XCTAssertEqual(statistics.pageouts, 0)
70 | XCTAssertEqual(statistics.faults, 31012)
71 | XCTAssertEqual(statistics.objectFiles, 5)
72 | XCTAssertEqual(statistics.objectFilesBytes, 140932)
73 | XCTAssertEqual(statistics.archiveFiles, 42)
74 | XCTAssertEqual(statistics.archiveFilesBytes, 24362016)
75 | XCTAssertEqual(statistics.dylibFiles, 87)
76 | XCTAssertEqual(statistics.wroteOutputFileBytes, 8758732)
77 | }
78 |
79 | private func getFakeClangSection(text: String, commandDescription: String) -> IDEActivityLogSection {
80 | return IDEActivityLogSection(sectionType: 1,
81 | domainType: "",
82 | title: "Clang Compilation",
83 | signature: "",
84 | timeStartedRecording: 0.0,
85 | timeStoppedRecording: 0.0,
86 | subSections: [],
87 | text: text,
88 | messages: [],
89 | wasCancelled: false,
90 | isQuiet: false,
91 | wasFetchedFromCache: false,
92 | subtitle: "",
93 | location: DVTDocumentLocation(documentURLString: "", timestamp: 0.0),
94 | commandDetailDesc: commandDescription,
95 | uniqueIdentifier: "",
96 | localizedResultString: "",
97 | xcbuildSignature: "",
98 | attachments: [],
99 | unknown: 0)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/LexRedactorTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 | import XCTest
22 | @testable import XCLogParser
23 |
24 | class LexRedactorTests: XCTestCase {
25 | let redactor = LexRedactor()
26 |
27 | func testRedacting() {
28 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path")
29 |
30 | XCTAssertEqual(redactedText, "Some /Users//path")
31 | }
32 |
33 | func testRedactingComplexUsername() {
34 | let redactedText = redactor.redactUserDir(string: "Some /Users/private-user/path")
35 |
36 | XCTAssertEqual(redactedText, "Some /Users//path")
37 | }
38 |
39 | func testRedactingHomePath() {
40 | let redactedText = redactor.redactUserDir(string: "/Users/private-user")
41 |
42 | XCTAssertEqual(redactedText, "/Users//")
43 | }
44 |
45 | func testMultiplePathsRedacting() {
46 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path and other /Users/private/path2")
47 |
48 | XCTAssertEqual(redactedText, "Some /Users//path and other /Users//path2")
49 | }
50 |
51 | func testRedactingFillsUserDir() {
52 | _ = redactor.redactUserDir(string: "Some /Users/private/path")
53 |
54 | XCTAssertEqual(redactor.userDirToRedact, "/Users/private/")
55 | }
56 |
57 | func testPredefinedUserDirIsRedacted() {
58 | redactor.userDirToRedact = "/Users/private/"
59 |
60 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path")
61 |
62 | XCTAssertEqual(redactedText, "Some /Users//path")
63 | }
64 |
65 | func testNotInPredefinedUserDirIsNotRedacted() {
66 | redactor.userDirToRedact = "/Users/priv/"
67 |
68 | let redactedText = redactor.redactUserDir(string: "Some /Users/private/path")
69 |
70 | XCTAssertEqual(redactedText, "Some /Users/private/path")
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/LogManifestTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import XCTest
21 | @testable import XCLogParser
22 |
23 | class LogManifestTests: XCTestCase {
24 |
25 | private static let logManifest = """
26 |
27 |
28 |
29 |
30 | logFormatVersion
31 | 11
32 | logs
33 |
34 | E8557234-04E4-40E7-A6D6-920AC64BCF21
35 |
36 | className
37 | IDEActivityLogSection
38 | documentTypeString
39 | <nil>
40 | domainType
41 | Xcode.IDEActivityLogDomainType.BuildLog
42 | fileName
43 | E8557234-04E4-40E7-A6D6-920AC64BCF21.xcactivitylog
44 | primaryObservable
45 |
46 | highLevelStatus
47 | W
48 |
49 | schemeIdentifier-containerName
50 | MyApp
51 | schemeIdentifier-schemeName
52 | MyApp
53 | schemeIdentifier-sharedScheme
54 | 1
55 | signature
56 | Build MyApp
57 | timeStartedRecording
58 | 579435259.73980498
59 | timeStoppedRecording
60 | 579435412.54278696
61 | title
62 | Build MyApp
63 | uniqueIdentifier
64 | E8557234-04E4-40E7-A6D6-920AC64BCF21
65 |
66 |
67 |
68 |
69 |
70 | """
71 |
72 | func testGetWithLogOptions() throws {
73 | let logDir = try TestUtils.createRandomTestDirWithPath("/DerivedData/ABC/Logs/Build")
74 | let logURL = logDir.appendingPathComponent("LogManifest.plist")
75 | try LogManifestTests.logManifest.write(to: logURL, atomically: true, encoding: .utf8)
76 |
77 | let logOptions = LogOptions(projectName: "",
78 | xcworkspacePath: "",
79 | xcodeprojPath: "",
80 | derivedDataPath: "",
81 | logManifestPath: logURL.path)
82 | let logEntries = try LogManifest().getWithLogOptions(logOptions)
83 | XCTAssertEqual(1, logEntries.count)
84 | }
85 |
86 | // swiftlint:disable function_body_length
87 | func testGetLatestLogEntry() throws {
88 | let firstStartedRecording = 570014939.87839794
89 | let firstStoppedRecording = 570014966.95137894
90 | let secondStartedRecording = 569841813.45673704
91 | let secondStoppedRecording = 569842953.09712994
92 |
93 | let mockLogEntries = ["599BC5A8-5E6A-4C16-A71E-A8D6301BAC07":
94 | ["className": "IDEActivityLogSection",
95 | "documentTypeString": "<nil>",
96 | "domainType": "Xcode.IDEActivityLogDomainType.BuildLog",
97 | "fileName": "599BC5A8-5E6A-4C16-A71E-A8D6301BAC07.xcactivitylog",
98 | "highLevelStatus": "E",
99 | "schemeIdentifier-containerName": "MyApp project",
100 | "schemeIdentifier-schemeName": "MyApp",
101 | "schemeIdentifier-sharedScheme": 1,
102 | "signature": "Build MyApp",
103 | "timeStartedRecording": firstStartedRecording,
104 | "timeStoppedRecording": firstStoppedRecording,
105 | "title": "Build My App",
106 | "uniqueIdentifier": "599BC5A8-5E6A-4C16-A71E-A8D6301BAC07"
107 | ], "D1FEAFFA-2E88-4221-9CD2-AB607529381D":
108 | ["className": "IDEActivityLogSection",
109 | "documentTypeString": "<nil>",
110 | "domainType": "Xcode.IDEActivityLogDomainType.BuildLog",
111 | "fileName": "D1FEAFFA-2E88-4221-9CD2-AB607529381D.xcactivitylog",
112 | "highLevelStatus": "E",
113 | "schemeIdentifier-containerName": "MyApp project",
114 | "schemeIdentifier-schemeName": "MyApp",
115 | "schemeIdentifier-sharedScheme": 1,
116 | "signature": "Build MyApp",
117 | "timeStartedRecording": secondStartedRecording,
118 | "timeStoppedRecording": secondStoppedRecording,
119 | "title": "Build My App",
120 | "uniqueIdentifier": "D1FEAFFA-2E88-4221-9CD2-AB607529381D"
121 | ]]
122 | let log: NSDictionary = ["logs": mockLogEntries]
123 | let logManifest = LogManifest()
124 | let logEntries = try logManifest.parse(dictionary: log, atPath: "")
125 | XCTAssertNotNil(logEntries)
126 | guard let latestLog = logEntries.first else {
127 | XCTFail("Latest log entry not found")
128 | return
129 | }
130 |
131 | let startDate = Date(timeIntervalSinceReferenceDate: firstStartedRecording)
132 | let endDate = Date(timeIntervalSinceReferenceDate: firstStoppedRecording)
133 | let calendar = Calendar.current
134 | guard let expectedDuration = calendar.dateComponents([.second], from: startDate, to: endDate).second else {
135 | XCTFail("Error creating an expected duration field")
136 | return
137 | }
138 | XCTAssertEqual(expectedDuration, latestLog.duration)
139 | }
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/ReporterTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import XCTest
21 |
22 | @testable import XCLogParser
23 |
24 | class ReporterTests: XCTestCase {
25 |
26 | func testMakeLogReporter() {
27 | let chromeTracerReporter = Reporter.chromeTracer.makeLogReporter()
28 | XCTAssertTrue(chromeTracerReporter is ChromeTracerReporter)
29 |
30 | let flatJsonReporter = Reporter.flatJson.makeLogReporter()
31 | XCTAssertTrue(flatJsonReporter is FlatJsonReporter)
32 |
33 | let htmlReporter = Reporter.html.makeLogReporter()
34 | XCTAssertTrue(htmlReporter is HtmlReporter)
35 |
36 | let jsonReporter = Reporter.json.makeLogReporter()
37 | XCTAssertTrue(jsonReporter is JsonReporter)
38 |
39 | let issuesReporter = Reporter.issues.makeLogReporter()
40 | XCTAssertTrue(issuesReporter is IssuesReporter)
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/String+BuildSpecificInformationRemovalTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2020 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import XCTest
21 | @testable import XCLogParser
22 |
23 | final class StringBuildSpecificInformationRemovalTest: XCTestCase {
24 | func testRemoveProductBuildIdentifierWithLogContainingProductBuildIdentifierRemovesProductBuildIdentifier() {
25 | let log = """
26 | Some /Library/Developer/Xcode/DerivedData/Product-bolnckhlbzxpxoeyfujluasoupft/Build/Intermediates.noindex/
27 | """
28 |
29 | let result = log.removeProductBuildIdentifier()
30 |
31 | XCTAssertEqual(result, "Some /Library/Developer/Xcode/DerivedData/Product/Build/Intermediates.noindex/")
32 | }
33 |
34 | func testRemoveProductBuildIdentifierWithLogContainingMultipleProductBuildIdentifiersRemovesAllOfThem() {
35 | let log = """
36 | Some /Library/Developer/Xcode/DerivedData/Product-bolnckhlbzxpxoeyfujluasoupft/Build/Intermediates.noindex/
37 | is not an object file (not allowed in a library)
38 | /Library/Developer/Xcode/DerivedData/Product-bouasoupft/Build/
39 | """
40 |
41 | let result = log.removeProductBuildIdentifier()
42 |
43 | XCTAssertEqual(result, """
44 | Some /Library/Developer/Xcode/DerivedData/Product/Build/Intermediates.noindex/
45 | is not an object file (not allowed in a library)
46 | /Library/Developer/Xcode/DerivedData/Product/Build/
47 | """)
48 | }
49 |
50 | func testRemoveProductBuildIdentifierWithLogNotContainingProductBuildIdentifierIsNoop() {
51 | let log = "Some /Library/Developer/Xcode/DerivedData/Product/Build/Intermediates.noindex/"
52 |
53 | let result = log.removeProductBuildIdentifier()
54 |
55 | XCTAssertEqual(result, log)
56 | }
57 |
58 | func testRemoveHexadecimalNumbersWithLogContainingHexadecimalNumberRemovesHexadecimalNumber() {
59 | let log = """
60 | UserInfo={NSUnderlyingError=0x7fcdc8712290 {Error Domain=kCFErrorDomainCFNetwork Code=-1003 "(null)"
61 | UserInfo={_kCFStreamErrorCodeKey=8, _kCFStreamErrorDomainKey=12}}
62 | """
63 |
64 | let result = log.removeHexadecimalNumbers()
65 |
66 | XCTAssertEqual(result, """
67 | UserInfo={NSUnderlyingError= {Error Domain=kCFErrorDomainCFNetwork Code=-1003 \"(null)\"
68 | UserInfo={_kCFStreamErrorCodeKey=8, _kCFStreamErrorDomainKey=12}}
69 | """)
70 | }
71 |
72 | func testRemoveHexadecimalNumbersWithLogContainingMultipleHexadecimalNumbersRemovesAllOfThem() {
73 | let log = """
74 | {NSUnderlyingError=0x7fb6d57290c0 {Error Domain=kCFErrorDomainCFNetwork Code=-1005 "(null)"
75 | UserInfo={NSErrorPeerAddressKey={length = 16, capacity = 16,
76 | bytes = 0x100200500aad1b7e0000000000000000}
77 | """
78 |
79 | let result = log.removeHexadecimalNumbers()
80 |
81 | XCTAssertEqual(result, """
82 | {NSUnderlyingError= {Error Domain=kCFErrorDomainCFNetwork Code=-1005 "(null)"
83 | UserInfo={NSErrorPeerAddressKey= []>{length = 16, capacity = 16,
84 | bytes = }
85 | """)
86 | }
87 |
88 | func testRemoveHexadecimalNumbersWithLogNotContainingHexadecimalNumbersIsNoop() {
89 | let log = """
90 | UserInfo={NSUnderlyingError=7fcdc8712290 {Error Domain=kCFErrorDomainCFNetwork Code=-1003 \"(null)\"
91 | UserInfo={_kCFStreamErrorCodeKey=8, _kCFStreamErrorDomainKey=12}}
92 | """
93 |
94 | let result = log.removeHexadecimalNumbers()
95 |
96 | XCTAssertEqual(result, log)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/SwiftCompilerParserTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import XCTest
21 | @testable import XCLogParser
22 |
23 | class SwiftCompilerParserTests: XCTestCase {
24 |
25 | let parser = SwiftCompilerParser()
26 |
27 | func testParseSwiftFunctionTimes() throws {
28 | try runParseSwiftFunctionTimesTest(rawFile: "myapp/MyView", escapedFile: "myapp/MyView")
29 | try runParseSwiftFunctionTimesTest(rawFile: "my app/MyView", escapedFile: "my%20app/MyView")
30 | }
31 |
32 | private func runParseSwiftFunctionTimesTest(rawFile: String,
33 | escapedFile: String,
34 | file: StaticString = #file,
35 | line: UInt = #line) throws {
36 | let emptylogSection = getFakeSwiftcSection(text: "text",
37 | commandDescription: "command")
38 | let text =
39 | "0.05ms\t/Users/user/\(rawFile).swift:9:9\tgetter textLabel\r" +
40 | "4.96ms\t/Users/user/\(rawFile).swift:11:14\tinitializer init(frame:)\r" +
41 | "0.04ms\t\tgetter None\r"
42 | let swiftTimesLogSection = getFakeSwiftcSection(text:
43 | text, commandDescription: "-debug-time-function-bodies")
44 |
45 | let duplicatedSwiftTimeslogSection = getFakeSwiftcSection(text:
46 | text, commandDescription: "-debug-time-function-bodies")
47 | let expectedFile = "file:///Users/user/\(escapedFile).swift"
48 | parser.addLogSection(emptylogSection)
49 | parser.addLogSection(swiftTimesLogSection)
50 | parser.addLogSection(duplicatedSwiftTimeslogSection)
51 |
52 | parser.parse()
53 | guard let functionTimes = parser.findFunctionTimesForFilePath(expectedFile) else {
54 | XCTFail("The command should have swiftc function times", file: file, line: line)
55 | return
56 | }
57 | XCTAssertEqual(2, functionTimes.count, file: file, line: line)
58 | let getter = functionTimes[0]
59 | let initializer = functionTimes[1]
60 | XCTAssertEqual(0.05, getter.durationMS, file: file, line: line)
61 | XCTAssertEqual(9, getter.startingLine, file: file, line: line)
62 | XCTAssertEqual(9, getter.startingColumn, file: file, line: line)
63 | XCTAssertEqual(2, getter.occurrences, file: file, line: line)
64 | XCTAssertEqual(expectedFile.removingPercentEncoding, getter.file, file: file, line: line)
65 | XCTAssertEqual("getter textLabel", getter.signature, file: file, line: line)
66 | XCTAssertEqual("initializer init(frame:)", initializer.signature, file: file, line: line)
67 | XCTAssertEqual(2, initializer.occurrences, file: file, line: line)
68 | }
69 |
70 | func testParseSwiftTypeCheckTimes() throws {
71 | try runTestParseSwiftTypeCheckTimes(rawFile: "project/CreatorHeaderViewModel",
72 | escapedFile: "project/CreatorHeaderViewModel")
73 | try runTestParseSwiftTypeCheckTimes(rawFile: "my project/CreatorHeaderViewModel",
74 | escapedFile: "my%20project/CreatorHeaderViewModel")
75 | }
76 |
77 | private func runTestParseSwiftTypeCheckTimes(rawFile: String,
78 | escapedFile: String,
79 | file: StaticString = #file,
80 | line: UInt = #line) throws {
81 | let emptylogSection = getFakeSwiftcSection(text: "text",
82 | commandDescription: "command")
83 |
84 | let swiftTimesLogSection = getFakeSwiftcSection(text:
85 | "0.72ms\t/Users/\(rawFile).swift:19:15\r",
86 | commandDescription: "-debug-time-expression-type-checking")
87 |
88 | let duplicatedSwiftTimeslogSection = getFakeSwiftcSection(text:
89 | "0.72ms\t/Users/\(rawFile).swift:19:15\r",
90 | commandDescription: "-debug-time-expression-type-checking")
91 | let expectedFile = "file:///Users/\(escapedFile).swift"
92 | parser.addLogSection(emptylogSection)
93 | parser.addLogSection(swiftTimesLogSection)
94 | parser.addLogSection(duplicatedSwiftTimeslogSection)
95 |
96 | parser.parse()
97 |
98 | guard let typeChecks = parser.findTypeChecksForFilePath(expectedFile)
99 | else {
100 | XCTFail("The command should have swiftc type check times")
101 | return
102 | }
103 | XCTAssertEqual(1, typeChecks.count)
104 | XCTAssertEqual(19, typeChecks[0].startingLine)
105 | XCTAssertEqual(15, typeChecks[0].startingColumn)
106 | XCTAssertEqual(0.72, typeChecks[0].durationMS)
107 | XCTAssertEqual(expectedFile.removingPercentEncoding, typeChecks[0].file)
108 | XCTAssertEqual(2, typeChecks[0].occurrences)
109 | }
110 |
111 | private func getFakeSwiftcSection(text: String, commandDescription: String) -> IDEActivityLogSection {
112 | return IDEActivityLogSection(sectionType: 1,
113 | domainType: "",
114 | title: "Swiftc Compilation",
115 | signature: "",
116 | timeStartedRecording: 0.0,
117 | timeStoppedRecording: 0.0,
118 | subSections: [],
119 | text: text,
120 | messages: [],
121 | wasCancelled: false,
122 | isQuiet: false,
123 | wasFetchedFromCache: false,
124 | subtitle: "",
125 | location: DVTDocumentLocation(documentURLString: "", timestamp: 0.0),
126 | commandDetailDesc: commandDescription,
127 | uniqueIdentifier: "",
128 | localizedResultString: "",
129 | xcbuildSignature: "",
130 | attachments: [],
131 | unknown: 0)
132 | }
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/TestUtils.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2019 Spotify AB.
2 | //
3 | // Licensed to the Apache Software Foundation (ASF) under one
4 | // or more contributor license agreements. See the NOTICE file
5 | // distributed with this work for additional information
6 | // regarding copyright ownership. The ASF licenses this file
7 | // to you under the Apache License, Version 2.0 (the
8 | // "License"); you may not use this file except in compliance
9 | // with the License. You may obtain a copy of the License at
10 | //
11 | // http://www.apache.org/licenses/LICENSE-2.0
12 | //
13 | // Unless required by applicable law or agreed to in writing,
14 | // software distributed under the License is distributed on an
15 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | // KIND, either express or implied. See the License for the
17 | // specific language governing permissions and limitations
18 | // under the License.
19 |
20 | import Foundation
21 |
22 | struct TestUtils {
23 |
24 | static func createRandomTestDir() throws -> URL {
25 | let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
26 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
27 | return url
28 | }
29 |
30 | static func createRandomTestDirWithPath(_ path: String) throws -> URL {
31 | let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
32 | let dirUrl = url.appendingPathComponent(path)
33 | try FileManager.default.createDirectory(at: dirUrl, withIntermediateDirectories: true, attributes: nil)
34 | return url
35 | }
36 |
37 | @discardableResult
38 | static func createSubdir(_ name: String, in dir: URL, attributes: [FileAttributeKey: Any]? = nil) throws -> URL {
39 | let url = dir.appendingPathComponent(name, isDirectory: true)
40 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
41 | return url
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/XCLogParserTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | #if !canImport(ObjectiveC)
2 | import XCTest
3 |
4 | extension ActivityParserTests {
5 | // DO NOT MODIFY: This is autogenerated, use:
6 | // `swift test --generate-linuxmain`
7 | // to regenerate.
8 | static let __allTests__ActivityParserTests = [
9 | ("testParseActivityLog", testParseActivityLog),
10 | ("testParseDBGConsoleLog", testParseDBGConsoleLog),
11 | ("testParseDVTTextDocumentLocation", testParseDVTTextDocumentLocation),
12 | ("testParseIBDocumentMemberLocation", testParseIBDocumentMemberLocation),
13 | ("testParseIDEActivityLogAnalyzerResultMessage", testParseIDEActivityLogAnalyzerResultMessage),
14 | ("testParseIDEActivityLogMessage", testParseIDEActivityLogMessage),
15 | ("testParseIDEActivityLogSection", testParseIDEActivityLogSection),
16 | ("testParseXcode3ProjectLocation", testParseXcode3ProjectLocation),
17 | ]
18 | }
19 |
20 | extension ChromeTracerOutputTests {
21 | // DO NOT MODIFY: This is autogenerated, use:
22 | // `swift test --generate-linuxmain`
23 | // to regenerate.
24 | static let __allTests__ChromeTracerOutputTests = [
25 | ("testTargetToTraceEvent", testTargetToTraceEvent),
26 | ]
27 | }
28 |
29 | extension IssuesReporterTests {
30 | // DO NOT MODIFY: This is autogenerated, use:
31 | // `swift test --generate-linuxmain`
32 | // to regenerate.
33 | static let __allTests__IssuesReporterTests = [
34 | ("testReport", testReport),
35 | ]
36 | }
37 |
38 | extension LexRedactorTests {
39 | // DO NOT MODIFY: This is autogenerated, use:
40 | // `swift test --generate-linuxmain`
41 | // to regenerate.
42 | static let __allTests__LexRedactorTests = [
43 | ("testMultiplePathsRedacting", testMultiplePathsRedacting),
44 | ("testNotInPredefinedUserDirIsNotRedacted", testNotInPredefinedUserDirIsNotRedacted),
45 | ("testPredefinedUserDirIsRedacted", testPredefinedUserDirIsRedacted),
46 | ("testRedacting", testRedacting),
47 | ("testRedactingComplexUsername", testRedactingComplexUsername),
48 | ("testRedactingFillsUserDir", testRedactingFillsUserDir),
49 | ("testRedactingHomePath", testRedactingHomePath),
50 | ]
51 | }
52 |
53 | extension LexerTests {
54 | // DO NOT MODIFY: This is autogenerated, use:
55 | // `swift test --generate-linuxmain`
56 | // to regenerate.
57 | static let __allTests__LexerTests = [
58 | ("testTokenizeClassName", testTokenizeClassName),
59 | ("testTokenizeClassNameRef", testTokenizeClassNameRef),
60 | ("testTokenizeDouble", testTokenizeDouble),
61 | ("testTokenizeError", testTokenizeError),
62 | ("testTokenizeInt", testTokenizeInt),
63 | ("testTokenizeList", testTokenizeList),
64 | ("testTokenizeListNil", testTokenizeListNil),
65 | ("testTokenizeString", testTokenizeString),
66 | ("testTokenizeStringRedacted", testTokenizeStringRedacted),
67 | ("testTokenizeStringWithTokenDelimiters", testTokenizeStringWithTokenDelimiters),
68 | ]
69 | }
70 |
71 | extension LogFinderTests {
72 | // DO NOT MODIFY: This is autogenerated, use:
73 | // `swift test --generate-linuxmain`
74 | // to regenerate.
75 | static let __allTests__LogFinderTests = [
76 | ("testGetLogManifestPathForNonExistingFile", testGetLogManifestPathForNonExistingFile),
77 | ("testGetLogManifestPathWithWorkspace", testGetLogManifestPathWithWorkspace),
78 | ("testGetLogManifestPathWithXcodeProj", testGetLogManifestPathWithXcodeProj),
79 | ("testGetLogsFromCustomDerivedData", testGetLogsFromCustomDerivedData),
80 | ("testGetProjectFolderWithHash", testGetProjectFolderWithHash),
81 | ("testLogsDirectoryForXcodeProject", testLogsDirectoryForXcodeProject),
82 | ]
83 | }
84 |
85 | extension LogManifestTests {
86 | // DO NOT MODIFY: This is autogenerated, use:
87 | // `swift test --generate-linuxmain`
88 | // to regenerate.
89 | static let __allTests__LogManifestTests = [
90 | ("testGetLatestLogEntry", testGetLatestLogEntry),
91 | ("testGetWithLogOptions", testGetWithLogOptions),
92 | ]
93 | }
94 |
95 | extension ParserTests {
96 | // DO NOT MODIFY: This is autogenerated, use:
97 | // `swift test --generate-linuxmain`
98 | // to regenerate.
99 | static let __allTests__ParserTests = [
100 | ("testBuildIdentifierShouldUseMachineName", testBuildIdentifierShouldUseMachineName),
101 | ("testDateFormatterUsesJSONFormat", testDateFormatterUsesJSONFormat),
102 | ("testGetIndividualSteps", testGetIndividualSteps),
103 | ("testParseAppCompilationTimes", testParseAppCompilationTimes),
104 | ("testParseAppNoopCompilationTimes", testParseAppNoopCompilationTimes),
105 | ("testParseInterfaceBuilderWarning", testParseInterfaceBuilderWarning),
106 | ("testParseNote", testParseNote),
107 | ("testParseSwiftIssuesDetails", testParseSwiftIssuesDetails),
108 | ("testParseTargetCompilationTimes", testParseTargetCompilationTimes),
109 | ("testParseTargetName", testParseTargetName),
110 | ("testParseWarningsAndErrors", testParseWarningsAndErrors),
111 | ]
112 | }
113 |
114 | extension ReporterTests {
115 | // DO NOT MODIFY: This is autogenerated, use:
116 | // `swift test --generate-linuxmain`
117 | // to regenerate.
118 | static let __allTests__ReporterTests = [
119 | ("testMakeLogReporter", testMakeLogReporter),
120 | ]
121 | }
122 |
123 | extension SwiftCompilerParserTests {
124 | // DO NOT MODIFY: This is autogenerated, use:
125 | // `swift test --generate-linuxmain`
126 | // to regenerate.
127 | static let __allTests__SwiftCompilerParserTests = [
128 | ("testParseSwiftFunctionTimes", testParseSwiftFunctionTimes),
129 | ("testParseSwiftTypeCheckTimes", testParseSwiftTypeCheckTimes),
130 | ]
131 | }
132 |
133 | public func __allTests() -> [XCTestCaseEntry] {
134 | return [
135 | testCase(ActivityParserTests.__allTests__ActivityParserTests),
136 | testCase(ChromeTracerOutputTests.__allTests__ChromeTracerOutputTests),
137 | testCase(IssuesReporterTests.__allTests__IssuesReporterTests),
138 | testCase(LexRedactorTests.__allTests__LexRedactorTests),
139 | testCase(LexerTests.__allTests__LexerTests),
140 | testCase(LogFinderTests.__allTests__LogFinderTests),
141 | testCase(LogManifestTests.__allTests__LogManifestTests),
142 | testCase(ParserTests.__allTests__ParserTests),
143 | testCase(ReporterTests.__allTests__ReporterTests),
144 | testCase(SwiftCompilerParserTests.__allTests__SwiftCompilerParserTests),
145 | ]
146 | }
147 | #endif
148 |
--------------------------------------------------------------------------------
/build_release_in_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker image build -t xclogparser .
4 | docker run -it --mount src="$(pwd)",target=/xclogparser,type=bind xclogparser bin/sh -c "cd xclogparser && swift build -c release"
5 |
6 | DESTINATION_PATH=releases/linux
7 | mkdir -p "$DESTINATION_PATH"
8 | cp .build/x86_64-unknown-linux/release/xclogparser "$DESTINATION_PATH"
9 | zip -X -r "$DESTINATION_PATH"/XCLogParser-x.x.x-Linux.zip "$DESTINATION_PATH"/xclogparser
--------------------------------------------------------------------------------
/ci/ci.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Source Helper Functions
4 | source "$(dirname "$0")/helpers.sh"
5 |
6 | # Only install tools when running on travis
7 | if [ -n "$TRAVIS_BUILD_ID" ]; then
8 | heading "Installing Tools"
9 | brew install swiftlint
10 | gem install xcpretty
11 | fi
12 |
13 | has_command swiftlint || fail "SwiftLint must be installed"
14 | has_command xcpretty || fail "xcpretty must be installed"
15 |
16 | #
17 | # Fail fast with swiftlint
18 | #
19 | heading "Linting"
20 |
21 | swiftlint lint --no-cache --strict || \
22 | fail "swiftlint failed"
23 |
24 | #
25 | # Build in release mode
26 | #
27 | heading "Building"
28 | set -o pipefail && rake build[release] | xcpretty || \
29 | fail "Release Build Failed"
30 |
31 | #
32 | # Run Tests
33 | #
34 | heading "Running Tests"
35 |
36 | set -o pipefail && rake test | xcpretty || \
37 | fail "Test Run failed"
38 |
--------------------------------------------------------------------------------
/ci/helpers.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | heading() {
4 | MAG='\033[0;35m'
5 | CLR='\033[0m'
6 | echo ""
7 | echo "${MAG}** $@ **${CLR}"
8 | echo ""
9 | }
10 |
11 | fail() {
12 | >&2 echo "error: $@"
13 | exit 1
14 | }
15 |
16 | has_command() {
17 | command -v "$1" >/dev/null 2>&1
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/docs/JSON Format.md:
--------------------------------------------------------------------------------
1 | # XCLogParser JSON Format
2 |
3 | A typical step is parsed and output as JSON with the following format:
4 | ```json
5 | {
6 | "detailStepType" : "swiftCompilation",
7 | "startTimestamp" : 1545143336.649699,
8 | "endTimestamp" : 1545143336.649699,
9 | "schema" : "MyApp",
10 | "domain" : "com.apple.dt.IDE.BuildLogSection",
11 | "parentIdentifier" : "095709ba230e4eda80ab43be3b68f99c_1545299644.4805899_20",
12 | "endDate" : "2018-12-18T14:28:56.650000+0000",
13 | "title" : "Compile \/Users\/\/projects\/MyApp\/Libraries\/Utilities\/Sources\/Disposables\/Cancelable.swift",
14 | "identifier" : "095709ba230e4eda80ab43be3b68f99c_1545299644.4805899_185",
15 | "signature" : "CompileSwift normal x86_64 \/Users\/\/MyApp\/Libraries\/Utilities\/Sources\/Disposables\/Cancelable.swift",
16 | "type" : "detail",
17 | "buildStatus" : "succeeded",
18 | "subSteps" : [
19 |
20 | ],
21 | "startDate" : "2018-12-18T14:28:56.650000+0000",
22 | "buildIdentifier" : "095709ba230e4eda80ab43be3b68f99c_1545299644.4805899",
23 | "machineName" : "095709ba230e4eda80ab43be3b68f99c",
24 | "duration" : 5.5941859483718872,
25 | "errorCount" : 0,
26 | "warningCount" : 0,
27 | "errors" : [],
28 | "warnings" : [],
29 | "fetchedFromCache" : false,
30 | "compilationEndTimestamp": 1545143336.649699,
31 | "compilationDuration": 5.5941859483718872,
32 | "swiftFunctionTimes" : [
33 | {
34 | "durationMS" : 0.08,
35 | "occurrences" : 5,
36 | "startingColumn" : 36,
37 | "startingLine" : 48,
38 | "file" : "file:\/\/\/Users\/\/MyApp\/Libraries\/Utilities\/Sources\/Disposables\/Cancelable.swift",
39 | "signature" : "getter description"
40 | }
41 | ],
42 | "swiftTypeCheckTimes" : [
43 | {
44 | "durationMS" : 0.5,
45 | "occurrences" : 2,
46 | "startingColumn" : 16,
47 | "startingLine" : 9,
48 | "file" : "file:\/\/\/Users\/\/MyApp\/Libraries\/Utilities\/Sources\/Disposables\/Cancelable.swift",
49 | }
50 | ]
51 | }
52 | ```
53 |
54 | The `type` field can assume three different values:
55 | - `main`: the summary of the whole build process. It's usually the Xcode scheme that was built.
56 | - `target`: a target that was built that belongs to a `main` type.
57 | - `detail`: a step inside the target. Usually a script that was run during a Pre build phase, the compilation of a single file inside the target or other Build Rule.
58 |
59 | Other fields:
60 | - `buildIdentifier`: a unique identifier for the given build. It uses the machine name plus the IDEActivityLog's unique identifier so it should be unique across different hosts.
61 | - `duration`: duration in seconds for the given step.
62 | - `subSteps`: an array of build steps that belong to the given one.
63 | - `parentIdentifier`: identifier of the step to which the given step belongs to.
64 | - `schema`: the name of the schema that was run.
65 | - `buildStatus`: `succeeded` or `failed`
66 | - `machineName`: the name of the host. If provided, it uses the value of the parameter `machine_name`, If not, it uses the name returned by `Host.current().localizedName`.
67 | - `signature`: for build steps of type `detail` it has the actual command executed.
68 | - `detailStepType`: only for build steps of type `detail` . It has some info about what was run inside that step.
69 | - `warningCount`: the number of warnings thrown by the compiler for the given step.
70 | - `warnings`: the list of warnings
71 | - `errorCount`: the number of errors for the given step.
72 | - `errors`: the list of errors.
73 | - `swiftFunctionTimes`: Optional. If the step is a `swiftCompilation` and the app was compiled with the flags `-Xfrontend -debug-time-function-bodies` it will show the list of functions and their compilation time. The `occurrences` is the number of times that function is compiled, so the effective time it took in your build is durationMS * ocurrences.
74 | - `swiftTypeCheckTimes`: Optional. If the step is a `swiftCompilation` and the app was compiled with the flags `-debug-time-expression-type-checking` it will show the list of functions and their compilation time. The `occurrences` is the number of times that check happens, so the effective time it took in your buikd is durationMS * ocurrences.
75 | - `fetchedFromCache`: For a `detail` step, `true` indicates that the file wasn't processed nor compiled but fetched from Xcode's internal cache. For a `main` or `target` step, `true` indicates that all its sub steps were fetched from cache, `false` that at least one sub step was proccesed or compiled.
76 | - `compilationEndTimestamp`: Timestamp in which the actual compilation finished. For a Target this could be before `endTimestamp` because in the new Xcode Build System linking can happen way after compilation.
77 | - `compilationDuration` Actual duration in seconds of just the compilation phase. In a Target this could be significant shorter than the `duration`.
78 |
79 | When possible, the `signature` content of `detail` steps is parsed to determine its type. This makes it easier to aggregate the data.
80 |
81 | Value | Description
82 | --- | ---
83 | cCompilation | An Objective-C, C or C++ file was compiled
84 | swiftCompilation | A Swift file was compiled
85 | scriptExecution | A Build phase script was ran
86 | createStaticLibrary | An Static library was created with Libtool
87 | linker | The linker ran
88 | copySwiftLibs | The Swift Runtime libs were copied
89 | compileAssetsCatalog | An Assets catalog was compiled
90 | compileStoryboard | An Storyboard file was compiled
91 | writeAuxiliaryFile | An Auxiliary file was copied into derived data
92 | linkStoryboards | Linking of Storyboards
93 | copyResourceFile | A Resource file was copied
94 | mergeSwiftModule | The merge swift module tool was executed
95 | XIBCompilation | A XIB file was compiled
96 | swiftAggregatedCompilation | Aggregated swift compilations
97 | precompileBridgingHeader | Bridging header was precompiled
98 | other | Neither of the above
99 | none | For steps that are not of type `detail`
100 |
101 |
--------------------------------------------------------------------------------
/docs/Xcactivitylog Format.md:
--------------------------------------------------------------------------------
1 | # The Xcactivitylog format
2 |
3 | Xcode logs are stored in files with the extension `.xcactivitylog`. Those files are gzipped to save storage. The files are encoded using a format called `SLF`.
4 |
5 | ## The SLF format
6 |
7 | This information was first documented by Vincent Isambart in this [blog post](https://techlife.cookpad.com/entry/2017/12/08/124532).
8 |
9 | A `SLF` document starts with the header `SLF0`. After the header, the document has a collection of encoded values. The SLF encoding format supports these types:
10 |
11 | - Integer
12 | - Double
13 | - String
14 | - Array
15 | - Class names
16 | - Class instances
17 | - Null
18 | - JSON
19 |
20 | A value encoded is formed by 3 parts:
21 |
22 | - Left hand side value (optional)
23 | - Character type delimiter
24 | - Right hand side value (optional)
25 |
26 | ### Integer
27 |
28 | - Character type delimiter: `#`
29 | - Example: `200#`
30 | - Left hand side value: An unsigned 64 bits integer.
31 |
32 | ### Double
33 |
34 | - Character type delimiter: `^`
35 | - Example: `afd021ebae48c141^`
36 | - Left hand side value: A little-endian floating point number, encoded in hexadecimal.
37 |
38 | You can convert it to a Swift Double using the `bitPattern` property of `Double`:
39 |
40 | ```swift
41 | guard let value = UInt64(input, radix: 16) else {
42 | return nil
43 | }
44 | let double = Double(bitPattern: value.byteSwapped)
45 | ```
46 |
47 | In the `xcactivitylog`'s files, this type of value is used to encode timestamps. Thus, the double represents a `timeInterval` value using `timeIntervalSinceReferenceDate`.
48 |
49 | ### Null
50 |
51 | - Character type delimiter: `-`
52 | - No left, nor right hand side value
53 |
54 | ### String
55 |
56 | - Character type delimiter: `"`
57 | - Example: `5"Hello`
58 | - Left hand side value: An `Integer` with the number of characters that are part of the `String`.
59 | - Right hand side value: The characters that are part of the `String`
60 |
61 | The number of characters works as in `NSString` rather than in `String`: it counts the 16-bit code units within the string’s UTF-16 representation and not the number of Unicode extended grapheme clusters within the string like in Swift's `String`.
62 |
63 | So you have to be careful to load the file not as an UTF-8 String, because it will give you a mismatch with the count in the `SLF` Format. Currently, we load the content of the file as an ASCII String to avoid that problem:
64 |
65 | ```swift
66 | let content = String(data: unzippedXcactivitylog, encoding: .ascii)
67 | ```
68 |
69 | Other example:
70 | `6"Hello--9#`
71 | In this case, there are three encoded values:
72 |
73 | 1. The String "Hello-"
74 | 2. A Null value.
75 | 3. The integer 9.
76 |
77 | ### Array
78 |
79 | - Character type delimiter: `(`
80 | - Example: `22(`
81 | - Left hand side value: An `Integer` with the number of elements that are part of the `Array`.
82 |
83 | The elements of an `Array` are `Class instances`
84 |
85 | ### JSON
86 |
87 | - Character type delimiter: `*`
88 | - Example: `"{\"wcStartTime\":732791618924407,\"maxRSS\":0,\"utime\":798,\"wcDuration\":852,\"stime\":798}"`
89 | - Left hand side value: An `Integer` with the number of characters that are part of the `JSON` string.
90 |
91 | The JSON is of the type `IDEFoundation.IDEActivityLogSectionAttachment`
92 |
93 | ### Class name
94 |
95 | - Character type delimiter: `%`
96 | - Example: `21%IDEActivityLogSection`
97 | - Left hand side value: An `Integer` with the number of characters that are part of the `Class name`.
98 | - Right hand side value: The characters that are part of the `Class name`
99 |
100 | It follows the same rules as a `String`.
101 |
102 | A given `Class name` only appears once: before its first `Class instance`. It's important to store the order in which you found a `Class name` in the log, because that index is used by the `Class instance`.
103 |
104 | ### Class instance
105 |
106 | - Character type delimiter: `@`
107 | - Example: `2@`
108 | - Left hand side value: An `Integer` with the index of the `Class name` of the `Class instance`'s type.
109 |
110 | In the case of `2@`, it means that the `Class instance`'s type is the `Class name` found in the 3rd position in the `SLF` document.
111 |
112 | ## Tokenizing the .xcactivitylog
113 |
114 | With those rules, you can decode the log and tokenize it. For instance, given this log's content:
115 |
116 | ```
117 | SLF010#21%IDEActivityLogSection1@0#39"Xcode.IDEActivityLogDomainType.BuildLog20"Build XCLogParserApp20"Build XCLogParserApp0074f8eaae48c141^8f19bcf4ae48c141^12(1@1#50"Xcode.IDEActivityLogDomainType.XCBuild.Preparation13"Prepare build13"Prepare build
118 | ```
119 |
120 | You can get these tokens:
121 |
122 | ```swift
123 | [type: "int", value: 10],
124 | [type: "className", name: "IDEActivityLogSection"],
125 | [type: "classInstance", className: "IDEActivityLogSection"],
126 | [type: "int", value: 0],
127 | [type: "string", value: "Xcode.IDEActivityLogDomainType.BuildLog"],
128 | [type: "string", value: "Build XCLogParserApp"],
129 | [type: "string", value: "Build XCLogParserApp"],
130 | [type: "double", value: 580158292.767495],
131 | [type: "double", value: 580158295.086277],
132 | [type: "array", count: 12],
133 | [type: "classInstance", className: "IDEActivityLogSection"],
134 | [type: "string", value: "Xcode.IDEActivityLogDomainType.XCBuild.Preparation"],
135 | [type: "string", value: "Prepare build"],
136 | [type: "string", value: "Prepare build"],
137 | ```
138 |
139 | The first integer is the version of the `SLF` format used. In Xcode 10.x and 11 Beta, the version is 10. The values after the version are the actual content of the log.
140 |
141 | ## Parsing an xcactivitylog
142 |
143 | One of the limitations of the `SLF` format is that it only points to the place where a `Class instance` starts, it doesn't have information about where it ends or about the name of its properties. The only information we have about the class instance we have is its type (the `Class name`).
144 |
145 | Inside the logs you can find these classes:
146 |
147 | - `IDEActivityLogSection`
148 | - `IDEActivityLogUnitTestSection`
149 | - `IDEActivityLogMessage`
150 | - `DVTDocumentLocation`
151 | - `DVTTextDocumentLocation`
152 | - `IDEActivityLogCommandInvocationSection`
153 | - `IDEActivityLogMajorGroupSection`
154 | - `IDEFoundation.IDEActivityLogSectionAttachment`
155 |
156 | If you search for them, you will find that they belong to the IDEFoundation.framework. A private framework part of Xcode. You can class dump it to get the headers of those classes. Once you have the headers, you will have the name and type of the properties that belong to the class. Now, you can match them to the tokens you got from the log. Some of them are in the same order than in the headers, but for others it will be about trial and error.
157 |
158 | In the project you can find those classes with their properties in the order in which they appear in the log in the file [IDEActivityModel.swift](../Sources/XCLogParser/activityparser/IDEActivityModel.swift).
159 |
--------------------------------------------------------------------------------
/images/kickstarter-ios-chrome-tracer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileNativeFoundation/XCLogParser/5facacc3813b38514e759c5ce5aec4b0fdcfa8d1/images/kickstarter-ios-chrome-tracer.png
--------------------------------------------------------------------------------
/images/kickstarter-ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileNativeFoundation/XCLogParser/5facacc3813b38514e759c5ce5aec4b0fdcfa8d1/images/kickstarter-ios.png
--------------------------------------------------------------------------------
/images/post-action-run-script.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileNativeFoundation/XCLogParser/5facacc3813b38514e759c5ce5aec4b0fdcfa8d1/images/post-action-run-script.png
--------------------------------------------------------------------------------
/run_in_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | docker run -it --mount src="$(pwd)",target=/xclogparser,type=bind xclogparser
4 |
--------------------------------------------------------------------------------