├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── comment.yml │ └── nightly.yml ├── LICENSE ├── README.md ├── shared.py └── version_compare.py /.gitignore: -------------------------------------------------------------------------------- 1 | less-*/ 2 | less*.zip 3 | compile.bat 4 | download.html 5 | __pycache__/ 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | -------------------------------------------------------------------------------- /.github/workflows/comment.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | issue_number: 5 | required: true 6 | owner: 7 | required: true 8 | repo: 9 | required: true 10 | body: 11 | required: true 12 | 13 | jobs: 14 | comment: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/github-script@v8 18 | with: 19 | github-token: ${{ secrets.WINGET_TOKEN }} 20 | script: | 21 | github.rest.issues.createComment({ 22 | issue_number: ${{ github.event.inputs.issue_number }}, 23 | owner: '${{ github.event.inputs.owner }}', 24 | repo: '${{ github.event.inputs.repo }}', 25 | body: '${{ github.event.inputs.body }}' 26 | }) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 J Taylor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # less-Windows [![nightly](https://github.com/jftuga/less-Windows/actions/workflows/nightly.yml/badge.svg)](https://github.com/jftuga/less-Windows/actions/workflows/nightly.yml) 2 | 3 | GNU [less](https://en.wikipedia.org/wiki/Less_\(Unix\)) compiled for Windows from the [less source](http://greenwoodsoftware.com/less/) via GitHub Actions. New versions are being checked daily, and builds are compiled with the latest version of Visual Studio. 4 | 5 | ## Installation 6 | 7 | Binaries for `less.exe` (and `lesskey.exe`) are provided on the [Releases Page](https://github.com/jftuga/less-Windows/releases). Download the appropriate one for your system. If you prefer to install less via a package manager, you can choose one of the following options: 8 | 9 | ### Winget 10 | 11 | A new version is pushed to the upstream [winget-pkgs](https://github.com/microsoft/winget-pkgs) for every release: 12 | 13 | ```powershell 14 | winget install jftuga.less 15 | ``` 16 | 17 | ### Chocolatey 18 | 19 | [less](https://community.chocolatey.org/packages/less) is available in the Community Repository: 20 | ```powershell 21 | choco install less 22 | ``` 23 | 24 | ### Scoop 25 | 26 | [less](https://scoop.sh/#/apps?q=main%2Fless&s=0&d=1&o=true) is available in the Main bucket: 27 | ```powershell 28 | scoop install less 29 | ``` 30 | -------------------------------------------------------------------------------- /shared.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r""" 4 | shared.py 5 | -John Taylor 6 | May-14-2020 7 | 8 | Functions shared between build.py and version_compare.py 9 | """ 10 | 11 | import re 12 | import time 13 | import urllib.request 14 | 15 | LESSURL = "http://greenwoodsoftware.com/less/download.html" 16 | version_url_re = re.compile(r"""Download RECOMMENDED version (.*?) """, re.M | re.S | re.I) 17 | NEWFILE = "new.txt" 18 | 19 | 20 | def download_less_web_page() -> str: 21 | """Download LESSURL and save the contents to fname 22 | 23 | Returns: 24 | An in-memory version of the downloaded web page 25 | """ 26 | 27 | fname = "download.html" 28 | try: 29 | urllib.request.urlretrieve(LESSURL, fname) 30 | time.sleep(1) 31 | except: 32 | return False 33 | 34 | try: 35 | with open(fname) as fp: 36 | page = fp.read() 37 | except: 38 | return False 39 | 40 | return page 41 | 42 | 43 | def get_latest_version_url(page: str) -> tuple: 44 | """Return the URL for the "RECOMMENDED version" 45 | 46 | Args: 47 | page: an HTML web page, provided in LESSURL 48 | 49 | Returns: 50 | A tuple containing: (version number, zip archive URL) 51 | Ex: 551, http://greenwoodsoftware.com/less/less-551.zip 52 | """ 53 | 54 | match = version_url_re.findall(page) 55 | if not len(match): 56 | return (None, None) 57 | 58 | version = match[0] 59 | archive = "less-%s.zip" % version 60 | url = LESSURL.replace("download.html", archive) 61 | return version, url 62 | -------------------------------------------------------------------------------- /version_compare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | r""" 4 | version_compare.py 5 | -John Taylor 6 | May-14-2020 7 | 8 | Compare local github version with less web site 9 | """ 10 | 11 | import json 12 | import urllib.request 13 | import re 14 | import sys 15 | from shared import download_less_web_page, get_latest_version_url, LESSURL, NEWFILE 16 | 17 | LOCALURL = "https://api.github.com/repos/jftuga/less-Windows/releases" 18 | 19 | 20 | def download_local_web_page() -> str: 21 | """Download and return JSON from LOCALURL 22 | """ 23 | try: 24 | with urllib.request.urlopen(LOCALURL) as f: 25 | page = f.read() 26 | except: 27 | return False 28 | 29 | page = page.decode("utf-8") 30 | return page 31 | 32 | 33 | def get_latest_local_version(page: str) -> str: 34 | """Extract and return the lastest release version from a JSON page 35 | Ex: 561 36 | """ 37 | try: 38 | j = json.loads(page) 39 | except: 40 | return False 41 | 42 | if not len(j): 43 | return "500" 44 | 45 | newest = j[0] 46 | print(f'{newest["tag_name"]=}') 47 | # The initial version is different than future versions 48 | if "v560" == newest["tag_name"]: 49 | return "560" 50 | 51 | # given less-v561.17, return 561 52 | release_version = newest["tag_name"][6:11] 53 | if release_version.endswith(".0"): 54 | release_version = re.sub(r"\.0$", "", release_version) 55 | print(f'{release_version=}') 56 | return release_version 57 | 58 | 59 | def main(): 60 | """Write to new.txt when a new version needs to be downloaded 61 | """ 62 | if not (page := download_local_web_page()): 63 | print("Unable to download URL: %s" % (LOCALURL)) 64 | sys.exit(10) 65 | 66 | if not (local_version := get_latest_local_version(page)): 67 | print("Unable to extract version from URL: %s" % (LOCALURL)) 68 | sys.exit(20) 69 | 70 | if not (page := download_less_web_page()): 71 | print("Unable to download URL: %s" % (LESSURL)) 72 | sys.exit(30) 73 | return 74 | 75 | remote_version, _ = get_latest_version_url(page) 76 | if remote_version is None: 77 | print("Unable to extract version from: %s" % (LESSURL), file=sys.stderr) 78 | sys.exit(40) 79 | 80 | if remote_version == local_version: 81 | print(f"Versions are the same: remote_version: {remote_version} local_version: {local_version}") 82 | return 83 | 84 | if float(local_version) >= float(remote_version): 85 | print(f"Local version is newer: local_version: {local_version} remote_version: {remote_version}") 86 | sys.exit(120) 87 | 88 | print(f"Remote version is newer: remote_version: {remote_version} local_version: {local_version}") 89 | print(f"Saving new version to file: {NEWFILE}") 90 | try: 91 | with open(NEWFILE, mode="w") as fp: 92 | fp.write("%s\n" % remote_version) 93 | except: 94 | print(f"Unable able to open file for writing: {NEWFILE}") 95 | sys.exit(50) 96 | 97 | 98 | if "__main__" == __name__: 99 | main() 100 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '0 0 * * *' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | checkver: 14 | runs-on: ubuntu-latest 15 | 16 | outputs: 17 | new_version: ${{ steps.check_new_version.outputs.new_version }} 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions/setup-python@v6 22 | - name: Compare versions 23 | run: python version_compare.py 24 | - name: Check if new version exists 25 | id: check_new_version 26 | run: | 27 | # If new.txt exists and is not empty 28 | if [ -s "new.txt" ]; then 29 | NEW_VERSION=$(cat new.txt) 30 | echo "New version is $NEW_VERSION" 31 | echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT 32 | fi 33 | 34 | build: 35 | runs-on: windows-${{ matrix.arch == 'arm' && '2022' || 'latest' }} 36 | needs: checkver 37 | if: ${{ needs.checkver.outputs.new_version }} 38 | strategy: 39 | matrix: 40 | arch: [x64, x86, arm64, arm] 41 | 42 | steps: 43 | - uses: actions/checkout@v6 44 | - uses: actions/setup-python@v6 45 | - uses: ilammy/msvc-dev-cmd@v1 46 | with: 47 | arch: ${{ matrix.arch != 'x64' && 'amd64_' || '' }}${{ matrix.arch }} 48 | sdk: ${{ matrix.arch == 'arm' && '10.0.22621.0' || '' }} 49 | 50 | - name: Build 51 | run: python .\build.py 52 | 53 | - name: Upload less to artifact 54 | uses: actions/upload-artifact@v6 55 | with: 56 | name: less-${{ matrix.arch }} 57 | path: | 58 | less.exe 59 | lesskey.exe 60 | 61 | release: 62 | needs: [checkver, build] 63 | if: ${{ github.event_name != 'pull_request' }} 64 | runs-on: ubuntu-latest 65 | permissions: 66 | contents: write 67 | discussions: write 68 | 69 | steps: 70 | - name: Get all artifacts 71 | uses: actions/download-artifact@v7 72 | - name: Zip each artifact 73 | run: find . -type d ! -path . -execdir zip -9 -rj "{}.zip" "{}" \; 74 | 75 | - uses: octokit/request-action@v2.x 76 | id: get_workflow_runtime 77 | with: 78 | route: GET /repos/{owner}/{repo}/actions/runs/{run_id} 79 | owner: ${{ github.repository_owner }} 80 | repo: less-Windows 81 | run_id: ${{ github.run_id }} 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | - id: extract_major_version 85 | run: | 86 | MAJOR_VERSION=$(echo ${{ needs.checkver.outputs.new_version }} | cut -d. -f1) 87 | echo "major_version=$MAJOR_VERSION" >> $GITHUB_OUTPUT 88 | 89 | - uses: softprops/action-gh-release@v2 90 | with: 91 | files: '*.zip' 92 | body: | 93 | Built with GitHub Actions at ${{ fromJson(steps.get_workflow_runtime.outputs.data).updated_at }} 94 | 95 | Release notes can be found [here](http://greenwoodsoftware.com/less/news.${{ steps.extract_major_version.outputs.major_version }}.html). 96 | tag_name: less-v${{ needs.checkver.outputs.new_version }} 97 | discussion_category_name: Announcements 98 | env: 99 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | 101 | winget: 102 | name: Publish to WinGet 103 | needs: [checkver, release] 104 | runs-on: ubuntu-latest 105 | 106 | steps: 107 | - uses: vedantmgoyal9/winget-releaser@main 108 | with: 109 | identifier: jftuga.less 110 | version: ${{ needs.checkver.outputs.new_version }} 111 | release-tag: less-v${{ needs.checkver.outputs.new_version }} 112 | installers-regex: '\.zip$' 113 | token: ${{ secrets.WINGET_TOKEN }} 114 | --------------------------------------------------------------------------------