Skip to content

Clean up the code and some of the API of compiling plugins, and improve persistence of cached compiler outputs #4290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

abertelrud
Copy link
Contributor

Motivation

Provide more information to clients of libSwiftPM even when a compiled plugin doesn't have to be recompiled because nothing has changed since the last time it was. Also make the API a bit cleaner.

Details

Clients now get the command line and compiler output even in the case of failure, and also when the plugin doesn't have to be recompiled because nothing changed (previously this information was lost). This is done by serializing a proper struct to JSON rather than just storing the hexadecimal representation of the configuration hash.

Callers also have control over the name of the temporary directory to represent the plugin, rather than always having it be the last path component of the sources directory.

The changes to the construction of the command line to compile plugins isn't nearly as extensive as it looks — it's mostly blankspace changes due to nesting.

Modifications:

  • persist the compilation state as a JSON-coded struct rather than just a hexadecimal input hash
  • let clients of PluginScriptRunner control the name of the intermediates directory (and not rely on a Sources type)
  • clean up the struct members in the compilation result to have clearer names
  • adjust and extend unit tests to cover cached information

Result:

[After your change, what will change.]

…ve cached compiler outputs

Clients now get the command line and compiler output even in the case of failure, and also when the plugin doesn't have to be recompiled because nothing changed (previously this information was lost).  This is done by serializing a proper struct to JSON rather than just storing the hexadecimal representation of the configuration hash.

Callers also have control over the name of the temporary directory to represent the plugin, rather than always having it be the last path component of the sources directory.

The changes to the construction of the command line to compile plugins isn't nearly as extensive as it looks — it's mostly blankspace changes due to nesting.
@abertelrud
Copy link
Contributor Author

@swift-ci please smoke test

@abertelrud abertelrud self-assigned this Apr 12, 2022
@@ -37,43 +37,46 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable {
self.cancellator = Cancellator(observabilityScope: .none)
}

/// Public protocol function that starts compiling the plugin script to an executable. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the completion handler on the callbackq queue when compilation ends.
/// Public protocol function that starts compiling the plugin script to an executable. The name is used as the basename for the executable and auxiliary files. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the completion handler on the callbackq queue after compilation ends.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes in this file are a bit hard to follow. are there specific changes you want the reviewers to focus on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, sorry about that — it's actually a bit easier when ignoring spaces, because the nesting changed.

Maybe one useful thing would be to see what is stored in the PersistedCompilationState struct. It doesn't yet have things like a fingerprint of the compiled plugin, but that's where it would be added. I think that is the most interesting thing to look at that isn't just cleaning up names and structure.

This is pretty well tested by unit tests that check for whether the compilation actually happened etc so I am not too concerned about the logic itself.

commandLine: commandLine,
environment: toolchain.swiftCompilerEnvironment,
inputHash: compilerInputHash,
duration: Double(startTime.distance(to: .now()).milliseconds() ?? 0) / 1000.0,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Millisecond precision seems enough here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps use DispatchTimeInterval so units as obvious?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, though because the idea is to encode this into JSON, that would lead to additional code to encode or decode it.

I looked at adding a fileprivate extension to DispatchTimeInterval to make it adopt Codable, but that's not permissible — we then have to make it codable for the whole module.

Since we're currently not using the duration maybe I will just take it out for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one way to do this is use a property wrapper which implements the serialization for the duration property. this is how we often deal with date formatting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. As this is unused, I'll go ahead and remove it, but I'll keep that in mind for next time.

}

// Check if we already have a compiled executable and a persisted state (we only recompile if things have changed).
let stateFilePath = self.cacheDir.appending(component: execName + ".state")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use "json" as extension, and names the file "state"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the literal name "state" and not based on the name of the executable? In that case we should maybe call the executable just "plugin". But that makes it harder to see which plugin it is when looking for example using top.

I was trying to keep all three files (executable, state, and diagnostics) with the same base name derived from the plugin name. So maybe just changing it to "json" here makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sry I meant execName + "-state.json"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense.

// We failed to read the `.inputhash` file. We warn about it but proceed with the compilation (a cache miss).
observabilityScope.emit(warning: "Couldn't read previous hash of plugin compilation inputs (\(error)")
// We couldn't write out the `.state` file. We warn about it but proceed.
observabilityScope.emit(debug: "... couldn't save plugin compilation state (\(error))")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the "..." prefix in these debug messages

Copy link
Contributor Author

@abertelrud abertelrud Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably just left over from print-style debugging (my indication for the end of an activity, whereas the beginning has a "..." suffix instead). I'll change it.

}
else {
// There is no need to recompile the executable, so we just call the completion handler with the results from last time.
// Return a PluginCompilationResult for both the successful and unsuccessful cases (to convey diagnostics, etc).
let result = PluginCompilationResult(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: return here?

Copy link
Contributor Author

@abertelrud abertelrud Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll move up the return.

var hostTriple: Triple {
return UserToolchain.default.triple
}

func compilePluginScript(sources: Sources, toolsVersion: ToolsVersion, observabilityScope: ObservabilityScope) throws -> PluginCompilationResult {
func compilePluginScript(sourceFiles: [AbsolutePath], pluginName: String, toolsVersion: ToolsVersion, observabilityScope: ObservabilityScope) throws -> PluginCompilationResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these kind of functions that involve I/O seems like they should be async

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the client calls that used this have been removed, so I can probably remove these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed the tsc_await to all the call sites.

@abertelrud
Copy link
Contributor Author

@swift-ci please smoke test

@abertelrud abertelrud force-pushed the eng/plugin-compilation-api-improvements branch from e64084f to 470eed8 Compare April 13, 2022 05:52
@abertelrud
Copy link
Contributor Author

@swift-ci please smoke test

@abertelrud
Copy link
Contributor Author

@tomerd Thanks for your feedback, I think I've addressed all of it.

@tomerd
Copy link
Contributor

tomerd commented Apr 13, 2022

👍

@abertelrud abertelrud merged commit 09d77a7 into swiftlang:main Apr 15, 2022
@abertelrud abertelrud deleted the eng/plugin-compilation-api-improvements branch April 15, 2022 19:07
@compnerd
Copy link
Member

compnerd added a commit that referenced this pull request Apr 15, 2022
…nd improve persistence of cached compiler outputs (#4290)"

This reverts commit 09d77a7.
abertelrud added a commit to abertelrud/swift-package-manager that referenced this pull request Apr 19, 2022
…ve persistence of cached compiler outputs (swiftlang#4290)

Clients now get the command line and compiler output even in the case of failure, and also when the plugin doesn't have to be recompiled because nothing changed (previously this information was lost).  This is done by serializing a proper struct to JSON rather than just storing the hexadecimal representation of the configuration hash.

Callers also have control over the name of the temporary directory to represent the plugin, rather than always having it be the last path component of the sources directory.

(cherry picked from commit 09d77a7)
abertelrud added a commit that referenced this pull request Apr 19, 2022
… improve persistence of cached compiler outputs (#4290 & #4299) (#4309)

* Clean up the code and some of the API of compiling plugins, and improve persistence of cached compiler outputs (#4290)

Clients now get the command line and compiler output even in the case of failure, and also when the plugin doesn't have to be recompiled because nothing changed (previously this information was lost).  This is done by serializing a proper struct to JSON rather than just storing the hexadecimal representation of the configuration hash.

Callers also have control over the name of the temporary directory to represent the plugin, rather than always having it be the last path component of the sources directory.

(cherry picked from commit 09d77a7)

* Fix breakage on Windows, which doesn't have `ProcessResult.ExitStatus.signalled`. (#4299)

(cherry picked from commit f9f74f1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants