diff --git a/.ci/ensure-go.sh b/.ci/ensure-go.sh new file mode 100755 index 0000000000..82b8603bc4 --- /dev/null +++ b/.ci/ensure-go.sh @@ -0,0 +1,8 @@ +if command -v dnf; then + dnf -y install golang gcc +elif command -v yum; then + yum install epel-release + yum -y install golang gcc +elif command -v apk; then + apk add go gcc +fi diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7931c4a57d..6d7e2e1929 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -8,21 +8,21 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: docker/metadata-action@v4 + - uses: docker/metadata-action@v5 id: meta with: images: tdewolff/minify - - uses: docker/login-action@v2 + - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - uses: docker/setup-buildx-action@v2 + - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v4 + - uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c91e3a4cc1..f9f0a4d9d4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: @@ -37,7 +37,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 879e154690..c075d96384 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -54,7 +54,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -91,7 +91,7 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000000..7cf93157de --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,248 @@ +name: Python + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - go_target: amd64 + cibw_target: x86_64 + #- go_target:'386 + # cibw_target: i686 + - go_target: arm64 + cibw_target: aarch64 + + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + if: matrix.go_target == 'arm64' + uses: docker/setup-qemu-action@v2 + with: + platforms: arm64 + - name: Build wheels + uses: pypa/cibuildwheel@v2.15.0 + with: + package-dir: bindings/py + env: + CIBW_ARCHS: ${{ matrix.cibw_target }} + CIBW_MANYLINUX_I686_IMAGE: quay.io/pypa/manylinux_2_28_i686 + CIBW_MANYLINUX_X86_64_IMAGE: quay.io/pypa/manylinux_2_28_x86_64 + CIBW_MANYLINUX_AARCH64_IMAGE: quay.io/pypa/manylinux_2_28_aarch64 + CIBW_BEFORE_ALL: .ci/ensure-go.sh; cd bindings/py; go build -buildmode=c-shared -o src/minify/_minify.so + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v0.1.14 + if: startsWith(github.ref, 'refs/tags/') + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels-linux-${{ matrix.go_target }} + path: ./wheelhouse/*.whl + + windows-go-crosscompile: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - go_target: amd64 + xcompiler: x86_64-w64-mingw32-gcc-win32 + # go_target: 386 + # xcompiler: i686-w64-mingw32-gcc-win32 + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v4 + with: + go-version: '>=1.17' + - name: Fetch go package + if: startsWith(github.ref, 'refs/tags/') + run: | + cd bindings/py + go get github.com/tdewolff/minify/v2@${GITHUB_REF#refs/tags/} + - name: Fetch go package + if: startsWith(github.ref, 'refs/tags/') == false + run: | + cd bindings/py + go get github.com/tdewolff/minify/v2@$(git describe --tags --abbrev=0) + - name: Prebuild windows extension + run: | + sudo apt-get install mingw-w64 + cd bindings/py + CC=${{ matrix.xcompiler }} CGO_ENABLED=1 GOOS=windows GOARCH=${{ matrix.go_target }} go build -buildmode=c-shared -o _minify.so + + - name: Upload go library + uses: actions/upload-artifact@v3 + with: + name: minify-windows-${{ matrix.go_target }}.so + path: bindings/py/_minify.so + + windows: + runs-on: windows-latest + needs: windows-go-crosscompile + strategy: + matrix: + include: + - go_target: amd64 + cibw_target: AMD64 + #- go_target: 386 + # cibw_target: x86 + #- go_target: arm64 + # cibw_target: ARM64 + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: minify-windows-${{ matrix.go_target }}.so + path: bindings/py/src/minify + + - name: Build wheels + uses: pypa/cibuildwheel@v2.15.0 + with: + package-dir: bindings/py + env: + CIBW_ARCHS: ${{ matrix.cibw_target }} + CIBW_BUILD_VERBOSITY: 1 + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels-windows-${{ matrix.go_target }} + path: ./wheelhouse/*.whl + + macos-go-crosscompile: + if: false + runs-on: ubuntu-latest + strategy: + matrix: + include: + - go_target: amd64 + clang_target: x86_64 + - go_target: arm64 + clang_target: arm64 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v4 + with: + go-version: '>=1.17' + - name: Fetch go package + if: startsWith(github.ref, 'refs/tags/') + run: | + cd bindings/py + go get github.com/tdewolff/minify/v2@${GITHUB_REF#refs/tags/} + - name: Fetch go package + if: startsWith(github.ref, 'refs/tags/') == false + run: | + cd bindings/py + go get github.com/tdewolff/minify/v2@$(git describe --tags --abbrev=0) + - name: Prebuild macos extension + run: | + cd bindings/py + export CGO_ENABLED=1 + export GOOS=darwin + export GOARCH=${{ matrix.go_target }} + #export CFLAGS="-Wno-error=unused-command-line-argument -target ${{ matrix.clang_target }}-unknown-darwin-unknown" + export CC=gcc + go build -buildmode=c-shared -o minify.so + + - name: Upload go library + uses: actions/upload-artifact@v3 + with: + name: minify-darwin-${{ matrix.go_target }}.so + path: bindings/py/minify.so + macos: + if: false + runs-on: macos-latest + needs: macos-go-crosscompile + strategy: + matrix: + include: + - go_target: amd64 + cibw_target: x86_64 + #- go_target: arm64 + # cibw_target: arm64 + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: minify-darwin-${{ matrix.go_target }}.so + path: bindings/py/src/minify/_minify.so + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + #- uses: actions/setup-go@v4 + # with: + # go-version: '>=1.17' + + - name: Build wheels + uses: pypa/cibuildwheel@v2.15.0 + with: + package-dir: bindings/py + env: + CIBW_ARCHS: ${{ matrix.cibw_target }} + #CIBW_ENVIRONMENT: GOARCH='${{ matrix.go_target }} CGO_ENABLED=1' + #CIBW_BEFORE_ALL: cd bindings/py; go build -buildmode=c-shared -o src/minify/_minify.so + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels-macos-${{ matrix.go_target }} + path: ./wheelhouse/*.whl + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: pip install build + - name: Build package + run: | + cd bindings/py + python -m build --sdist + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: bindings/py/dist/* + + release: + name: Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + #needs: [linux, windows, macos, sdist] + needs: [linux, windows, sdist] + steps: + - name: Collect artifacts + uses: actions/download-artifact@v3 + with: + path: dist + - name: Omit non-release files + run: rm dist/*.so + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v0.1.14 + with: + files: dist/* + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index c9ea38dbc1..e2cf6cf8ef 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,10 @@ bindings/js/example/node_modules bindings/js/example/test.min.html bindings/py/go.mod bindings/py/go.sum -bindings/py/minify.h -bindings/py/minify.so -bindings/py/tdewolff_minify.egg-info +bindings/py/**/*.h +bindings/py/**/*.so +bindings/py/**/*.egg-info bindings/py/example/example.min.html bindings/py/dist +bindings/py/build +bindings/py/**/*.pyc diff --git a/bindings/js/package-lock.json b/bindings/js/package-lock.json index 4eede1f77a..fa15ab5b95 100644 --- a/bindings/js/package-lock.json +++ b/bindings/js/package-lock.json @@ -11,10 +11,10 @@ "license": "MIT", "dependencies": { "node-gyp": "^9.4.0", - "node-gyp-build": "^4.6.0" + "node-gyp-build": "^4.6.1" }, "devDependencies": { - "node-api-headers": "^1.0.1" + "node-api-headers": "^1.1.0" } }, "node_modules/@isaacs/cliui": { @@ -805,9 +805,9 @@ } }, "node_modules/node-api-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.0.1.tgz", - "integrity": "sha512-42iSgdJwCPp2eDYB4jrj8tJ5SjakZA41I7QxilOXa8E1fPbKyfr7hG4VzikCableU/HUqpnwmoqGNVnUYYbeIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.1.0.tgz", + "integrity": "sha512-ucQW+SbYCUPfprvmzBsnjT034IGRB2XK8rRc78BgjNKhTdFKgAwAmgW704bKIBmcYW48it0Gkjpkd39Azrwquw==", "dev": true }, "node_modules/node-gyp": { @@ -835,9 +835,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -1955,9 +1955,9 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "node-api-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.0.1.tgz", - "integrity": "sha512-42iSgdJwCPp2eDYB4jrj8tJ5SjakZA41I7QxilOXa8E1fPbKyfr7hG4VzikCableU/HUqpnwmoqGNVnUYYbeIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.1.0.tgz", + "integrity": "sha512-ucQW+SbYCUPfprvmzBsnjT034IGRB2XK8rRc78BgjNKhTdFKgAwAmgW704bKIBmcYW48it0Gkjpkd39Azrwquw==", "dev": true }, "node-gyp": { @@ -1979,9 +1979,9 @@ } }, "node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==" + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==" }, "nopt": { "version": "6.0.0", diff --git a/bindings/js/package.json b/bindings/js/package.json index 8b0efcf5cc..270f9a9430 100644 --- a/bindings/js/package.json +++ b/bindings/js/package.json @@ -48,9 +48,9 @@ ], "dependencies": { "node-gyp": "^9.4.0", - "node-gyp-build": "^4.6.0" + "node-gyp-build": "^4.6.1" }, "devDependencies": { - "node-api-headers": "^1.0.1" + "node-api-headers": "^1.1.0" } } diff --git a/bindings/py/MANIFEST.in b/bindings/py/MANIFEST.in index 811bb886ee..f8a50bf715 100644 --- a/bindings/py/MANIFEST.in +++ b/bindings/py/MANIFEST.in @@ -1,3 +1,5 @@ include README.md -include minify.go minify.c go.mod go.sum -include py.typed minify.pyi +include minify.go go.mod go.sum +include src/minify/_minify.so +include build_minify.py +include py.typed diff --git a/bindings/py/build_minify.py b/bindings/py/build_minify.py new file mode 100644 index 0000000000..1ce9a3f64f --- /dev/null +++ b/bindings/py/build_minify.py @@ -0,0 +1,14 @@ +import cffi + +C_DEFS = """ +char * minifyConfig(char **ckeys, char **cvals, long long length); +char * minifyString(char *cmediatype, char *cinput, long long input_length, char *coutput, long long *output_length); +char * minifyFile(char *cmediatype, char *cinput, char *coutput); +""" + +ffi = cffi.FFI() +ffi.set_source('minify._ffi_minify', None) +ffi.cdef(C_DEFS) + +if __name__ == '__main__': + ffi.compile() diff --git a/bindings/py/go.mod b/bindings/py/go.mod index 8e99ddd2cc..6c419b1598 100644 --- a/bindings/py/go.mod +++ b/bindings/py/go.mod @@ -3,6 +3,6 @@ module github.com/tdewolff/minify/bindings/py go 1.18 require ( - github.com/tdewolff/minify/v2 v2.12.2 - github.com/tdewolff/parse/v2 v2.6.3 + github.com/tdewolff/minify/v2 v2.12.9 + github.com/tdewolff/parse/v2 v2.6.8 ) diff --git a/bindings/py/go.sum b/bindings/py/go.sum index 8f8e038021..9b54af9cf9 100644 --- a/bindings/py/go.sum +++ b/bindings/py/go.sum @@ -8,8 +8,13 @@ github.com/tdewolff/minify/v2 v2.12.1 h1:zcjJTcO0uI+asdT+nd4TjXi3KUmVV/G2kxOKKrg github.com/tdewolff/minify/v2 v2.12.1/go.mod h1:p5pwbvNs1ghbFED/ZW1towGsnnWwzvM8iz8l0eURi9g= github.com/tdewolff/minify/v2 v2.12.2 h1:AKIoVwJj/HgBm+d/fPqpEZ31EtCM5FJfJNGagdR9Ecg= github.com/tdewolff/minify/v2 v2.12.2/go.mod h1:p5pwbvNs1ghbFED/ZW1towGsnnWwzvM8iz8l0eURi9g= +github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RUB4zA= +github.com/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU= github.com/tdewolff/parse/v2 v2.6.3 h1:O5rshbkaRmpRtD7k2lG65bEJpcfUMNg5Cx2uRKWVsI8= github.com/tdewolff/parse/v2 v2.6.3/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs= +github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA= +github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM= github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/bindings/py/minify.c b/bindings/py/minify.c deleted file mode 100644 index d2284e9a67..0000000000 --- a/bindings/py/minify.c +++ /dev/null @@ -1,148 +0,0 @@ -#include - -char *minifyConfig(char **, char **, long long); -char *minifyString(char *, char *, long long, char *, long long *); -char *minifyFile(char *, char *, char *); - -PyObject *config(PyObject *self, PyObject *args) { - PyObject *pyconfig; - if (PyArg_ParseTuple(args, "O", &pyconfig) == 0 || !PyDict_Check(pyconfig)) { - PyErr_SetString(PyExc_ValueError, "expected config argument"); - return NULL; - } else if (!PyDict_Check(pyconfig)) { - PyErr_SetString(PyExc_ValueError, "config must be a dict[str,str|bool|int]"); - return NULL; - } - - Py_ssize_t length = PyDict_Size(pyconfig); - const char **keys = (const char **)malloc(length * sizeof(const char *)); - const char **vals = (const char **)malloc(length * sizeof(const char *)); - - Py_ssize_t pos = 0; - PyObject *pykey, *pyval; - while (PyDict_Next(pyconfig, &pos, &pykey, &pyval)) { - const char *key = PyUnicode_AsUTF8(pykey); // handles deallocation - if (key == NULL) { - PyErr_SetString(PyExc_ValueError, "config must be a dict[str,str|bool|int]"); - free(vals); - free(keys); - return NULL; - } - keys[pos-1] = key; - - int decref = 0; - if (PyBool_Check(pyval) || PyLong_Check(pyval)) { - pyval = PyObject_Str(pyval); - if (pyval == NULL) { - PyErr_SetString(PyExc_ValueError, "config must be a dict[str,str|bool|int]"); - free(vals); - free(keys); - return NULL; - } - decref = 1; - } - const char *val = PyUnicode_AsUTF8(pyval); // handles deallocation - if (val == NULL) { - PyErr_SetString(PyExc_ValueError, "config must be a dict[str,str|bool|int]"); - free(vals); - free(keys); - return NULL; - } - vals[pos-1] = val; - - if (decref == 1) { - Py_DECREF(pyval); - } - } - - char *error = minifyConfig((char **)keys, (char **)vals, length); - free(vals); - free(keys); - if (error != NULL) { - PyErr_SetString(PyExc_ValueError, error); - free(error); - return NULL; - } - Py_RETURN_NONE; -} - -PyObject *string(PyObject *self, PyObject *args) { - PyObject *pymediatype, *pyinput; - if (PyArg_ParseTuple(args, "OO", &pymediatype, &pyinput) == 0) { - PyErr_SetString(PyExc_ValueError, "expected mediatype and input arguments"); - return NULL; - } - - const char *mediatype = PyUnicode_AsUTF8(pymediatype); // handles deallocation - if (mediatype == NULL) { - PyErr_SetString(PyExc_ValueError, "mediatype must be a string"); - return NULL; - } - - Py_ssize_t input_length; // not including trailing NULL-byte - const char *input = PyUnicode_AsUTF8AndSize(pyinput, &input_length); // handles deallocation - if (input == NULL) { - PyErr_SetString(PyExc_ValueError, "input must be a string"); - return NULL; - } - - long long output_length; // not including trailing NULL-byte - char *output = (char *)malloc(input_length); - char *error = minifyString((char *)mediatype, (char *)input, (long long)input_length, output, &output_length); - if (error != NULL) { - PyErr_SetString(PyExc_ValueError, error); - free(error); - return NULL; - } - - PyObject *pyoutput = PyUnicode_DecodeUTF8(output, (Py_ssize_t)output_length, NULL); - free(output); - return pyoutput; -} - -PyObject *file(PyObject *self, PyObject *args) { - PyObject *pymediatype, *pyinput, *pyoutput; - if (PyArg_ParseTuple(args, "OOO", &pymediatype, &pyinput, &pyoutput) == 0) { - PyErr_SetString(PyExc_ValueError, "expected mediatype, input, and output arguments"); - return NULL; - } - - const char *mediatype = PyUnicode_AsUTF8(pymediatype); // handles deallocation - if (mediatype == NULL) { - return NULL; - } - - const char *input = PyUnicode_AsUTF8(pyinput); // handles deallocation - if (input == NULL) { - return NULL; - } - - const char *output = PyUnicode_AsUTF8(pyoutput); // handles deallocation - if (output == NULL) { - return NULL; - } - - char *error = minifyFile((char *)mediatype, (char *)input, (char *)output); - if (error != NULL) { - PyErr_SetString(PyExc_ValueError, error); - free(error); - return NULL; - } - Py_RETURN_NONE; -} - -static PyMethodDef MinifyMethods[] = { - {"config", config, METH_VARARGS, "Configure minify options."}, - {"string", string, METH_VARARGS, "Minify string."}, - {"file", file, METH_VARARGS, "Minify file."}, - {NULL, NULL, 0, NULL} -}; - -static struct PyModuleDef minifymodule = { - PyModuleDef_HEAD_INIT, "minify", NULL, -1, MinifyMethods -}; - -PyMODINIT_FUNC -PyInit_minify(void) { - return PyModule_Create(&minifymodule); -} diff --git a/bindings/py/minify.go b/bindings/py/minify.go index 9a161d40ce..99bd23b716 100644 --- a/bindings/py/minify.go +++ b/bindings/py/minify.go @@ -1,6 +1,5 @@ package main -// #cgo pkg-config: python3-embed import "C" import ( "fmt" diff --git a/bindings/py/minify.pyi b/bindings/py/minify.pyi deleted file mode 100644 index ebd8ae8fd2..0000000000 --- a/bindings/py/minify.pyi +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Dict, Union - -def config(configuration: Dict[str, Union[str,bool,int]]) -> None: ... -def string(mediatype: str, string: str) -> str: ... -def file(mediatype: str, input_filename: str, output_filename: str) -> None: ... diff --git a/bindings/py/setup.py b/bindings/py/setup.py index e86e2a95de..0374277305 100644 --- a/bindings/py/setup.py +++ b/bindings/py/setup.py @@ -1,21 +1,14 @@ import pathlib -from subprocess import call -from setuptools import Extension, setup +from setuptools import setup from setuptools.command.build_ext import build_ext from setuptools.errors import CompileError +from setuptools.extension import Extension +from subprocess import CalledProcessError, check_call + HERE = pathlib.Path(__file__).parent README = (HERE / "README.md").read_text() -class build_go_ext(build_ext): - """Custom command to build extension from Go source files""" - def build_extension(self, ext): - ext_path = self.get_ext_fullpath(ext.name) - cmd = ['go', 'build', '-buildmode=c-shared', '-o', ext_path] - #cmd += ext.sources - out = call(cmd) - if out != 0: - raise CompileError('Go build failed') def get_version(): with open('go.mod') as f: @@ -25,6 +18,17 @@ def get_version(): return line.split()[1][1:] raise CompileError('Version retrieval failed') + +class build_ext_external(build_ext): + """Placeholder for externally-built extension.""" + def build_extension(self, ext: Extension): + if not all(pathlib.Path(p).exists() for p in ext.sources): + try: + check_call(['go', 'build', '-buildmode=c-shared', '-o', *ext.sources]) + except CalledProcessError as e: + raise CompileError('Go compilation failed!') from e + + setup( name="tdewolff-minify", version=get_version(), @@ -35,9 +39,34 @@ def get_version(): author="Taco de Wolff", author_email="tacodewolff@gmail.com", license="MIT", + classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: JavaScript", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Pre-processors", + "Topic :: Text Processing :: Markup", + ], ext_modules=[ - Extension("minify", ["minify.go"]), + Extension("minify", ["src/minify/_minify.so"]), ], - cmdclass={"build_ext": build_go_ext}, + cmdclass={"build_ext": build_ext_external}, + packages=["minify"], + package_dir={"": "src"}, + include_package_data=True, + setup_requires=["cffi>=1.0.0"], + cffi_modules=["build_minify.py:ffi"], + install_requires=["cffi>=1.0.0"], zip_safe=False, ) diff --git a/bindings/py/src/minify/__init__.py b/bindings/py/src/minify/__init__.py new file mode 100644 index 0000000000..5f72ae7052 --- /dev/null +++ b/bindings/py/src/minify/__init__.py @@ -0,0 +1,75 @@ +import pathlib +from typing import Dict, Union + +from ._ffi_minify import ffi + +__all__ = ['MinifyError', 'config', 'file', 'string'] + + +lib = ffi.dlopen(str(pathlib.Path(__file__).parent / '_minify.so')) + + +class MinifyError(ValueError): + """ + Exception raised when error occurs during minification. + """ + + +def _check_error(cdata): + if cdata: + raise MinifyError(ffi.string(cdata).decode()) + + +def config(configuration: Dict[str, Union[str,bool,int]]) -> None: + """ + Configure minifier behavior. + + Supported configuration keys: + * css-precision (int) + * html-keep-comments (bool) + * html-keep-conditional-comments (bool) + * html-keep-default-attr-vals (bool) + * html-keep-document-tags (bool) + * html-keep-end-tags (bool) + * html-keep-whitespace (bool) + * html-keep-quotes (bool) + * js-precision (int) + * js-keep-var-names (bool) + * js-version (int) + * json-precision (int) + * json-keep-numbers (bool) + * svg-keep-comments (bool) + * svg-precision (int) + * xml-keep-whitespace (bool) + """ + length = len(configuration) + err_msg = lib.minifyConfig( + [ffi.new('char[]', k.encode()) for k in configuration.keys()], + [ffi.new('char[]', str(v).encode()) for v in configuration.values()], + length) + _check_error(err_msg) + + +def string(mediatype: str, string: str) -> str: + """ + Minify code from a string. + + The minifier backend will be selected based on the specified MIME type. + """ + input_bytes = string.encode() + input_len = len(input_bytes) + output_bytes = ffi.new(f'char[{input_len}]') + output_len = ffi.new('long long *') + err_msg = lib.minifyString(mediatype.encode(), input_bytes, input_len, output_bytes, output_len) + _check_error(err_msg) + return ffi.string(output_bytes, output_len[0]).decode() + + +def file(mediatype: str, input_filename: str, output_filename: str) -> None: + """ + Minify a file and write output to another file. + + The minifier backend will be selected based on the specified MIME type. + """ + err_msg = lib.minifyFile(mediatype.encode(), input_filename.encode(), output_filename.encode()) + _check_error(err_msg)