diff --git a/pyproject.toml b/pyproject.toml index 4fd106947..16a6ac5c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.1" +version = "2.10.2" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/_cli/cli_pack.py b/src/uipath/_cli/cli_pack.py index 974948902..84525d8be 100644 --- a/src/uipath/_cli/cli_pack.py +++ b/src/uipath/_cli/cli_pack.py @@ -292,24 +292,11 @@ def pack_fn( ) for file in files: - if file.is_binary: - # Read binary files in binary mode - with open(file.file_path, "rb") as f: - z.writestr(f"content/{file.relative_path}", f.read()) - else: - try: - # Try UTF-8 first - with open(file.file_path, "r", encoding="utf-8") as f: - z.writestr(f"content/{file.relative_path}", f.read()) - except UnicodeDecodeError: - # If UTF-8 fails, try with utf-8-sig (for files with BOM) - try: - with open(file.file_path, "r", encoding="utf-8-sig") as f: - z.writestr(f"content/{file.relative_path}", f.read()) - except UnicodeDecodeError: - # If that also fails, try with latin-1 as a fallback - with open(file.file_path, "r", encoding="latin-1") as f: - z.writestr(f"content/{file.relative_path}", f.read()) + archive_path = f"content/{file.relative_path}" + if os.sep != "/": + archive_path = archive_path.replace(os.sep, "/") + with open(file.file_path, "rb") as f: + z.writestr(archive_path, f.read()) def display_project_info(config): diff --git a/tests/cli/test_pack.py b/tests/cli/test_pack.py index 0c34ab2ee..44809c8cb 100644 --- a/tests/cli/test_pack.py +++ b/tests/cli/test_pack.py @@ -326,6 +326,55 @@ def test_include_file_extensions( "Binary file content was corrupted during packing" ) + def test_pack_preserves_crlf_line_endings( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + ) -> None: + """Test that packing preserves CRLF line endings byte-for-byte. + + Regression test: text-mode reads on Windows silently convert CRLF to LF, + causing files to be trimmed. The pack must use binary-mode reads so the + zip entry is byte-identical to the file on disk. + """ + crlf_content = b"line1\r\nline2\r\nline3\r\n" + lf_content = b"line1\nline2\nline3\n" + + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump(create_uipath_json(), f) + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + # Write main.py with CRLF endings using binary mode to avoid any conversion + with open("main.py", "wb") as f: + f.write(b"def main(input):\r\n return input\r\n") + # Write a helper file with CRLF endings + with open("crlf_file.py", "wb") as f: + f.write(crlf_content) + # Write a helper file with LF endings + with open("lf_file.py", "wb") as f: + f.write(lf_content) + create_bindings_file() + + with patch("uipath._cli.cli_init.Middlewares.next") as mock_middleware: + mock_middleware.return_value = MiddlewareResult(should_continue=True) + init_result = runner.invoke(cli, ["init"], env={}) + assert init_result.exit_code == 0 + + result = runner.invoke(cli, ["pack", "./"], env={}) + assert result.exit_code == 0 + + with zipfile.ZipFile( + f".uipath/{project_details.name}.{project_details.version}.nupkg", "r" + ) as z: + assert z.read("content/crlf_file.py") == crlf_content, ( + "CRLF line endings were not preserved during packing" + ) + assert z.read("content/lf_file.py") == lf_content, ( + "LF line endings were not preserved during packing" + ) + def test_include_files( self, runner: CliRunner, diff --git a/uv.lock b/uv.lock index d5b170efa..0757c5b0f 100644 --- a/uv.lock +++ b/uv.lock @@ -2531,7 +2531,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.1" +version = "2.10.2" source = { editable = "." } dependencies = [ { name = "applicationinsights" },