From a9ce535d7ab81d42c4adf58053c8f1b0481edc03 Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Wed, 11 Dec 2024 16:18:47 +0100 Subject: [PATCH] fix(composer): Restore any modified files after analysis Introduce a `LockfileProvider` that ensures a lockfile to be present by either using the existing one, or enabling the creation of one and creating it. A self-created lockfile is deleted afterwards, and any modifications to the definition file are reverted. Signed-off-by: Sebastian Schuberth --- .../composer/src/main/kotlin/Composer.kt | 35 ++------ .../src/main/kotlin/LockfileProvider.kt | 86 +++++++++++++++++++ 2 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 plugins/package-managers/composer/src/main/kotlin/LockfileProvider.kt diff --git a/plugins/package-managers/composer/src/main/kotlin/Composer.kt b/plugins/package-managers/composer/src/main/kotlin/Composer.kt index 6c5e00075bb85..57fcc3b0116e3 100644 --- a/plugins/package-managers/composer/src/main/kotlin/Composer.kt +++ b/plugins/package-managers/composer/src/main/kotlin/Composer.kt @@ -51,10 +51,8 @@ import org.ossreviewtoolkit.utils.ort.showStackTrace import org.semver4j.RangesList import org.semver4j.RangesListFactory -import org.semver4j.Semver private const val COMPOSER_PHAR_BINARY = "composer.phar" -private const val COMPOSER_LOCK_FILE = "composer.lock" private const val SCOPE_NAME_REQUIRE = "require" private const val SCOPE_NAME_REQUIRE_DEV = "require-dev" private val ALL_SCOPE_NAMES = setOf(SCOPE_NAME_REQUIRE, SCOPE_NAME_REQUIRE_DEV) @@ -139,7 +137,11 @@ class Composer( } val lockfile = stashDirectories(workingDir.resolve("vendor")).use { _ -> - ensureLockfile(workingDir).let { + val lockfileProvider = LockfileProvider(definitionFile) + + requireLockfile(workingDir) { lockfileProvider.lockfile.isFile } + + lockfileProvider.ensureLockfile { logger.info { "Parsing lockfile at '$it'..." } parseLockfile(it.readText()) } @@ -242,33 +244,6 @@ class Composer( scopeDependencies = scopes ) } - - private fun ensureLockfile(workingDir: File): File { - val lockfile = workingDir.resolve(COMPOSER_LOCK_FILE) - - val hasLockfile = lockfile.isFile - requireLockfile(workingDir) { hasLockfile } - if (hasLockfile) return lockfile - - // Ensure that the build is not configured to disallow the creation of lockfiles. - ComposerCommand.run(workingDir, "--no-interaction", "config", "--unset", "lock").requireSuccess() - - val composerVersion = Semver(ComposerCommand.getVersion(workingDir)) - val args = buildList { - add("--no-interaction") - add("update") - add("--ignore-platform-reqs") - - if (composerVersion.major >= 2) { - add("--no-install") - add("--no-audit") - } - } - - ComposerCommand.run(workingDir, *args.toTypedArray()).requireSuccess() - - return lockfile - } } /** diff --git a/plugins/package-managers/composer/src/main/kotlin/LockfileProvider.kt b/plugins/package-managers/composer/src/main/kotlin/LockfileProvider.kt new file mode 100644 index 0000000000000..47fb188edd7e5 --- /dev/null +++ b/plugins/package-managers/composer/src/main/kotlin/LockfileProvider.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.packagemanagers.composer + +import java.io.File + +import kotlin.io.path.moveTo + +import org.semver4j.Semver + +private const val COMPOSER_LOCK_FILE = "composer.lock" + +class LockfileProvider(private val definitionFile: File) { + private val workingDir = definitionFile.parentFile + + val lockfile = workingDir.resolve(COMPOSER_LOCK_FILE) + + fun ensureLockfile(block: (File) -> T): T { + if (lockfile.isFile) return block(lockfile) + + val definitionFileBackup = enableLockfileCreation() + + return try { + require(createLockFile()) + block(lockfile) + } finally { + lockfile.delete() + definitionFileBackup?.also { it.toPath().moveTo(definitionFile.toPath(), overwrite = true) } + } + } + + private fun enableLockfileCreation(): File? { + var definitionFileBackup: File? = null + val lockConfig = ComposerCommand.run(workingDir, "--no-interaction", "config", "lock") + + if (lockConfig.isSuccess && lockConfig.stdout.trim() == "false") { + File.createTempFile("composer", "json", workingDir).also { + // The above call already creates an empty file, so the copy call needs to overwrite it. + definitionFileBackup = definitionFile.copyTo(it, overwrite = true) + } + + // Ensure that the build is not configured to disallow the creation of lockfiles. + val unsetLock = ComposerCommand.run(workingDir, "--no-interaction", "config", "--unset", "lock") + if (unsetLock.isError) { + definitionFileBackup?.delete() + return null + } + } + + return definitionFileBackup + } + + private fun createLockFile(): Boolean { + val args = buildList { + add("--no-interaction") + add("update") + add("--ignore-platform-reqs") + + val composerVersion = Semver(ComposerCommand.getVersion(workingDir)) + if (composerVersion.major >= 2) { + add("--no-install") + add("--no-audit") + } + } + + val update = ComposerCommand.run(workingDir, *args.toTypedArray()) + return update.isSuccess + } +}