diff --git a/.github/scripts/validate-cci-patch-ng.sh b/.github/scripts/validate-cci-patch-ng.sh new file mode 100644 index 0000000..85abefe --- /dev/null +++ b/.github/scripts/validate-cci-patch-ng.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Get TEST_ALL_RECIPES from environment variable, default to 0 (false) +PYTHON_NG_TEST_ALL_RECIPES=${PYTHON_NG_TEST_ALL_RECIPES:-0} + +SAMPLE_RECIPES_NUM=30 +RECIPES_BUILD_NUM=10 +RECIPES_BUILT_COUNT=0 + +# Ensure required tools are installed +COMMANDS=("conan" "yq" "jq") +for cmd in "${COMMANDS[@]}"; do + if ! which $cmd &> /dev/null; then + echo "ERROR: $cmd is not installed. Please install $cmd to proceed." + exit 1 + fi +done + +# Find all conanfile.py files that use apply_conandata_patches +RECIPES=$(find . -type f -name "conanfile.py" -exec grep -l "apply_conandata_patches(self)" {} + | sort | uniq) +# And does not need system requirement +RECIPES=$(grep -L "/system" $RECIPES) +# And does not contain Conan 1 imports +RECIPES=$(grep -L "from conans" $RECIPES) + +echo "Found $(echo "$RECIPES" | wc -l) recipes using apply_conandata_patches." + +if [ "${PYTHON_NG_TEST_ALL_RECIPES}" -eq "1" ]; then + SAMPLE_RECIPES_NUM=$(echo "$RECIPES" | wc -l) + RECIPES_BUILD_NUM=$SAMPLE_RECIPES_NUM + echo "PYTHON_NG_TEST_ALL_RECIPES is set to 1, testing all $SAMPLE_RECIPES_NUM recipes." +else + RECIPES=$(shuf -e ${RECIPES[@]} -n $SAMPLE_RECIPES_NUM) + echo "Pick $SAMPLE_RECIPES_NUM random recipes to test:" + echo "$RECIPES" +fi + +# Run conan create for each sampled recipe +for it in $RECIPES; do + + if [ $RECIPES_BUILT_COUNT -ge $RECIPES_BUILD_NUM ]; then + echo "Reached the limit of $RECIPES_BUILD_NUM recipes built, stopping. All done." + break + fi + + recipe_dir=$(dirname "${it}") + pushd "$recipe_dir" > /dev/null + echo "Testing recipe in directory: ${recipe_dir}" + # Get a version from conandata.yml that uses a patch + version=$(yq '.patches | keys | .[0]' conandata.yml 2>/dev/null) + if [ -z "$version" ]; then + echo "ERROR: No patches found in conandata.yml for $recipe_dir, skipping." + popd > /dev/null + continue + fi + version=$(echo ${version} | tr -d '"') + # Replace apply_conandata_patches to exit just after applying patches + sed -i -e 's/apply_conandata_patches(self)/apply_conandata_patches(self); import sys; sys.exit(0)/g' conanfile.py + + # Allow conan create to fail without stopping the script, we will handle errors manually + set +e + + # Create the package with the specified version + output=$(conan create . --version=${version} 2>&1) + # Accept some errors as non-fatal + if [ $? -ne 0 ]; then + echo "WARNING: conan create failed for $recipe_dir" + allowed_errors=( + "ERROR: There are invalid packages" + "ERROR: Version conflict" + "ERROR: Missing binary" + "Failed to establish a new connection" + "ConanException: sha256 signature failed" + "ConanException: Error downloading file" + "ConanException: Cannot find" + "certificate verify failed: certificate has expired" + "NotFoundException: Not found" + ) + # check if any allowed error is in the output + if printf '%s\n' "${allowed_errors[@]}" | grep -q -f - <(echo "$output"); then + echo "WARNING: Could not apply patches, skipping build:" + echo "$output" | tail -n 10 + echo "-------------------------------------------------------" + else + echo "ERROR: Fatal error during conan create command execution:" + echo "$output" + popd > /dev/null + exit 1 + fi + else + echo "INFO: Successfully patched $recipe_dir." + echo "$output" | tail -n 10 + echo "-------------------------------------------------------" + RECIPES_BUILT_COUNT=$((RECIPES_BUILT_COUNT + 1)) + fi + popd > /dev/null +done \ No newline at end of file diff --git a/.github/workflows/conan-center-index.yml b/.github/workflows/conan-center-index.yml new file mode 100644 index 0000000..0b9c5c0 --- /dev/null +++ b/.github/workflows/conan-center-index.yml @@ -0,0 +1,47 @@ +name: Validate Applying Patch in Conan Center Index + +on: + push: + paths-ignore: + - 'doc/**' + - '**/*.md' + - 'LICENSE' + - 'example/**' + - '.gitignore' + - 'tests/**' + workflow_dispatch: + pull_request: + paths-ignore: + - 'doc/**' + - '**/*.md' + - 'LICENSE' + - 'example/**' + - '.gitignore' + - 'tests/**' + +jobs: + conan-center-index-validate: + name: "Validate Patche-NG in Conan Center Index" + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + path: python-ng + + - name: Setup Conan client + uses: conan-io/setup-conan@v1 + + - name: Setup Python NG + run: pip install -e ./python-ng + + - name: Checkout conan-center-index + uses: actions/checkout@v5 + with: + repository: conan-io/conan-center-index + path: conan-center-index + ref: 'd8efbb6f3c51b134205f01d3f8d90bdad1a67fe6' + + - name: Validate Python-NG patch application + working-directory: conan-center-index/recipes + run: bash "${GITHUB_WORKSPACE}/python-ng/.github/scripts/validate-cci-patch-ng.sh" diff --git a/.github/workflows/conan.yml b/.github/workflows/conan.yml new file mode 100644 index 0000000..e952949 --- /dev/null +++ b/.github/workflows/conan.yml @@ -0,0 +1,48 @@ +name: Validate Conan client + +on: + push: + paths-ignore: + - 'doc/**' + - '**/*.md' + - 'LICENSE' + - 'example/**' + - '.gitignore' + - 'tests/**' + workflow_dispatch: + pull_request: + paths-ignore: + - 'doc/**' + - '**/*.md' + - 'LICENSE' + - 'example/**' + - '.gitignore' + - 'tests/**' + +jobs: + conan-client-validate: + name: "Validate Conan client" + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v5 + + - name: Setup python + uses: actions/setup-python@v6 + with: + python-version: 3.12 + architecture: x64 + + - name: Clone conan client + run: | + git clone --depth 1 https://github.com/conan-io/conan.git + cd conan/ + pip install -r conans/requirements_dev.txt + pip install -r conans/requirements.txt + + - name: Run Conan client tests involving patch-ng + run: | + pip install -e . + cd conan/ + pytest -v test/functional/test_third_party_patch_flow.py + pytest -v test/functional/tools/test_files.py + pytest -v test/unittests/tools/files/test_patches.py diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index b5cac3a..1d9bb40 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,4 +1,4 @@ -name: Main workflow +name: Python Patch-NG Tests on: push: diff --git a/patch_ng.py b/patch_ng.py index 7312ba1..334e238 100755 --- a/patch_ng.py +++ b/patch_ng.py @@ -29,7 +29,7 @@ SOFTWARE. """ __author__ = "Conan.io " -__version__ = "1.19.0-dev" +__version__ = "1.19.0" __license__ = "MIT" __url__ = "https://github.com/conan-io/python-patch" @@ -900,6 +900,9 @@ def _normalize_filenames(self): p.source = xnormpath(p.source) p.target = xnormpath(p.target) + p.source = p.source.strip(b'"') + p.target = p.target.strip(b'"') + sep = b'/' # sep value can be hardcoded, but it looks nice this way # references to parent are not allowed diff --git a/tests/filewithspace/0001-quote.diff b/tests/filewithspace/0001-quote.diff new file mode 100644 index 0000000..c7d51e0 --- /dev/null +++ b/tests/filewithspace/0001-quote.diff @@ -0,0 +1,6 @@ +diff '--color=auto' -ruN "b/Wrapper/FreeImage.NET/cs/Samples/Sample 01 - Loading and saving/Program.cs" "a/Wrapper/FreeImage.NET/cs/Samples/Sample 01 - Loading and saving/Program.cs" +--- "b/Wrapper/FreeImage.NET/cs/Samples/Sample 01 - Loading and saving/Program.cs" 2025-10-08 15:56:02.302486070 +0200 ++++ "a/Wrapper/FreeImage.NET/cs/Samples/Sample 01 - Loading and saving/Program.cs" 2025-10-08 15:21:46.283174211 +0200 +@@ -1 +1 @@ +-feriunt summos, fulmina montes. ++lux oculorum laetificat animam. diff --git a/tests/run_tests.py b/tests/run_tests.py index 38f5e3c..b942a09 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -589,6 +589,40 @@ def test_add_move_and_update_file(self): content = f.read() self.assertTrue(b'dum tempus habemus, operemur bonum' in content) +class TestPatchFileWithSpaces(unittest.TestCase): + + def setUp(self): + self.save_cwd = os.getcwd() + self.tmpdir = mkdtemp(prefix=self.__class__.__name__) + shutil.copytree(join(TESTS, 'filewithspace'), join(self.tmpdir, 'filewithspace')) + patch_folder = join(self.tmpdir, "a", "Wrapper", "FreeImage.NET", "cs", "Samples", "Sample 01 - Loading and saving") + os.makedirs(patch_folder, exist_ok=True) + self.program_cs = join(patch_folder, "Program.cs") + with open(self.program_cs, 'w') as fd: + fd.write("feriunt summos, fulmina montes.") + + def tearDown(self): + os.chdir(self.save_cwd) + remove_tree_force(self.tmpdir) + + def test_patch_with_white_space(self): + """When a patch file is generated using `diff -ruN b/ a/` command, and + contains white spaces in the file path, the patch should be applied correctly. + + Reported by https://github.com/conan-io/conan/issues/16727 + """ + + os.chdir(self.tmpdir) + print("TMPDIR:", self.tmpdir) + pto = patch_ng.fromfile(join(self.tmpdir, 'filewithspace', '0001-quote.diff')) + self.assertEqual(len(pto), 1) + self.assertEqual(pto.items[0].type, patch_ng.PLAIN) + self.assertTrue(pto.apply()) + with open(self.program_cs, 'rb') as f: + content = f.read() + self.assertTrue(b'lux oculorum laetificat animam.' in content) + + class TestHelpers(unittest.TestCase): # unittest setting longMessage = True