diff --git a/.github/scripts/build-macos.sh b/.github/scripts/build-macos.sh index bb4c1e0c..fae7e27f 100755 --- a/.github/scripts/build-macos.sh +++ b/.github/scripts/build-macos.sh @@ -1,27 +1,52 @@ #!/bin/sh -xe -mkdir build || true -cd build - -OSX_TARGET="10.9" - +ENABLE_SANITIZERS="OFF" if [ "$1" = "release" ]; then - BUILD_TYPE=RelWithDebInfo - cmake \ - -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_TARGET}" \ - -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - -DENABLE_OPENMP="ON" \ - -DENABLE_LTO="ON" \ - .. + BUILD_TYPE="RelWithDebInfo" + ENABLE_LTO="ON" else - BUILD_TYPE=Debug - cmake \ - -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_TARGET}" \ - -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ - -DENABLE_OPENMP="ON" \ - -DENABLE_SANITIZERS="ON" \ - .. + BUILD_TYPE="Debug" + ENABLE_LTO="OFF" fi -cmake --build . --config "${BUILD_TYPE}" -- -j$(nproc) -make -j$(nproc) test_solvespace +# this is an option for our Github CI only, since it doesn't have a macos arm64 image yet +CMAKE_GENERATOR="Unix Makefiles" +CMAKE_PREFIX_PATH="" +if [ "$2" = "arm64" ]; then + OSX_ARCHITECTURE="arm64" + CMAKE_PREFIX_PATH="/tmp/libomp-arm64/libomp/11.0.1" + git apply cmake/libpng-macos-arm64.patch || echo "Could not apply patch, probably already patched..." + mkdir build-arm64 || true + cd build-arm64 +elif [ "$2" = "x86_64" ]; then + OSX_ARCHITECTURE="x86_64" + CMAKE_PREFIX_PATH="/tmp/libomp-x86_64/libomp/11.0.1" + mkdir build || true + cd build +else + mkdir build || true + cd build +fi + +if [ "$3" = "xcode" ]; then + CMAKE_GENERATOR="Xcode" +fi + +cmake \ + -G "${CMAKE_GENERATOR}" \ + -D CMAKE_PREFIX_PATH="${CMAKE_PREFIX_PATH}" \ + -D CMAKE_OSX_ARCHITECTURES="${OSX_ARCHITECTURE}" \ + -D CMAKE_BUILD_TYPE="${BUILD_TYPE}" \ + -D ENABLE_OPENMP="ON" \ + -D ENABLE_SANITIZERS="${ENABLE_SANITIZERS}" \ + -D ENABLE_LTO="${ENABLE_LTO}" \ + .. + +if [ "$3" = "xcode" ]; then + open solvespace.xcodeproj +else + cmake --build . --config "${BUILD_TYPE}" -j$(sysctl -n hw.logicalcpu) + if [ $(uname -m) = "$2" ]; then + make -j$(sysctl -n hw.logicalcpu) test_solvespace + fi +fi \ No newline at end of file diff --git a/.github/scripts/install-macos.sh b/.github/scripts/install-macos.sh index 457dd5d4..c6ec104d 100755 --- a/.github/scripts/install-macos.sh +++ b/.github/scripts/install-macos.sh @@ -1,4 +1,14 @@ #!/bin/sh -xe -brew install libomp -git submodule update --init \ No newline at end of file +if [ "$1" = "ci" ]; then + curl -L https://bintray.com/homebrew/bottles/download_file?file_path=libomp-11.0.1.arm64_big_sur.bottle.tar.gz --output /tmp/libomp-arm64.tar.gz + mkdir /tmp/libomp-arm64 || true + tar -xzvf /tmp/libomp-arm64.tar.gz -C /tmp/libomp-arm64 + curl -L https://bintray.com/homebrew/bottles/download_file?file_path=libomp-11.0.1.big_sur.bottle.tar.gz --output /tmp/libomp-x86_64.tar.gz + mkdir /tmp/libomp-x86_64 || true + tar -xzvf /tmp/libomp-x86_64.tar.gz -C /tmp/libomp-x86_64 +else + brew install libomp +fi + +git submodule update --init extlib/cairo extlib/freetype extlib/libdxfrw extlib/libpng extlib/mimalloc extlib/pixman extlib/zlib diff --git a/.github/scripts/sign-macos.sh b/.github/scripts/sign-macos.sh index c23dab99..80ac1256 100755 --- a/.github/scripts/sign-macos.sh +++ b/.github/scripts/sign-macos.sh @@ -1,5 +1,19 @@ #!/bin/bash -xe +lipo \ + -create \ + build/bin/SolveSpace.app/Contents/MacOS/SolveSpace \ + build-arm64/bin/SolveSpace.app/Contents/MacOS/SolveSpace \ + -output \ + build/bin/SolveSpace.app/Contents/MacOS/SolveSpace + +lipo \ + -create \ + build/bin/SolveSpace.app/Contents/MacOS/solvespace-cli \ + build-arm64/bin/SolveSpace.app/Contents/MacOS/solvespace-cli \ + -output \ + build/bin/SolveSpace.app/Contents/MacOS/solvespace-cli + cd build openmp="bin/SolveSpace.app/Contents/Resources/lib/libomp.dylib" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 154b6faa..99c85b3d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -32,14 +32,14 @@ jobs: shell: bash test_macos: - runs-on: macos-latest + runs-on: macos-10.15 name: Test macOS steps: - uses: actions/checkout@v2 - name: Install Dependencies - run: .github/scripts/install-macos.sh + run: .github/scripts/install-macos.sh ci - name: Build & Test - run: .github/scripts/build-macos.sh + run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64 build_release_windows: needs: [test_ubuntu, test_windows, test_macos] @@ -80,13 +80,13 @@ jobs: build_release_macos: needs: [test_ubuntu, test_windows, test_macos] name: Build Release macOS - runs-on: macos-latest + runs-on: macos-10.15 steps: - uses: actions/checkout@v2 - name: Install Dependencies - run: .github/scripts/install-macos.sh + run: .github/scripts/install-macos.sh ci - name: Build & Test - run: .github/scripts/build-macos.sh release + run: .github/scripts/build-macos.sh release arm64 && .github/scripts/build-macos.sh release x86_64 - name: Sign Build run: .github/scripts/sign-macos.sh env: @@ -100,7 +100,7 @@ jobs: with: name: macos path: build/bin/SolveSpace.dmg - + deploy_snap_amd64: needs: [test_ubuntu, test_windows, test_macos] name: Deploy AMD64 Snap @@ -128,7 +128,7 @@ jobs: store_login: ${{ secrets.SNAPSTORE_LOGIN }} snap: ${{ steps.build.outputs.snap }} release: edge,beta - + deploy_snap_arm64: needs: [test_ubuntu, test_windows, test_macos] name: Deploy ARM64 Snap @@ -160,7 +160,7 @@ jobs: store_login: ${{ secrets.SNAPSTORE_LOGIN }} snap: ${{ steps.build.outputs.snap }} release: edge,beta - + update_edge_release: name: Update Edge Release needs: [build_release_windows, build_release_windows_openmp, build_release_macos] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27462aef..1ca64b3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,11 +34,11 @@ jobs: shell: bash test_macos: - runs-on: macos-latest + runs-on: macos-10.15 name: Test macOS steps: - uses: actions/checkout@v2 - name: Install Dependencies - run: .github/scripts/install-macos.sh + run: .github/scripts/install-macos.sh ci - name: Build & Test - run: .github/scripts/build-macos.sh + run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64 diff --git a/CMakeLists.txt b/CMakeLists.txt index 12948781..62eb646d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,5 @@ # cmake configuration +cmake_minimum_required(VERSION 3.9...3.19) if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) message(FATAL_ERROR @@ -7,17 +8,10 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) " mkdir build && cd build && cmake ..") endif() -cmake_minimum_required(VERSION 3.7.2 FATAL_ERROR) -if(NOT CMAKE_VERSION VERSION_LESS 3.11.0) - cmake_policy(VERSION 3.11.0) -endif() -if(NOT CMAKE_VERSION VERSION_LESS 3.9) - # LTO/IPO with non-Intel compilers on Linux requires policy CMP0069 to be set to NEW. - # Set it explicitly until cmake_minimum_required is raised to >= 3.9. - cmake_policy(SET CMP0069 NEW) -endif() set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") + +cmake_policy(SET CMP0048 OLD) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED YES) @@ -27,7 +21,7 @@ set(CMAKE_USER_MAKE_RULES_OVERRIDE set(CMAKE_USER_MAKE_RULES_OVERRIDE_CXX "${CMAKE_SOURCE_DIR}/cmake/cxx_flag_overrides.cmake") -if(APPLE OR CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") +if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() @@ -40,10 +34,10 @@ include(GetGitCommitHash) # and instead uncomment the following, adding the complete git hash of the checkout you are using: # set(GIT_COMMIT_HASH 0000000000000000000000000000000000000000) -project(solvespace) set(solvespace_VERSION_MAJOR 3) set(solvespace_VERSION_MINOR 0) string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH) +project(solvespace LANGUAGES C CXX ASM) set(ENABLE_GUI ON CACHE BOOL "Whether the graphical interface is enabled") @@ -68,10 +62,6 @@ if("${CMAKE_GENERATOR}" STREQUAL "Xcode") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_BINARY_DIR}/bin>) endif() -if(NOT CMAKE_C_COMPILER_ID STREQUAL CMAKE_CXX_COMPILER_ID) - message(FATAL_ERROR "C and C++ compilers should be supplied by the same vendor") -endif() - if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0) # GCC 4.8/4.9 ship with broken but present . meh. @@ -210,6 +200,12 @@ if(WIN32 OR APPLE) find_vendored_package(PNG libpng SKIP_INSTALL_ALL ON PNG_LIBRARY png_static + PNG_ARM_NEON "off" + PNG_SHARED OFF + PNG_STATIC ON + PNG_EXECUTABLES OFF + PNG_TESTS OFF + PNG_FRAMEWORK OFF PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng) list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng) @@ -222,11 +218,14 @@ if(WIN32 OR APPLE) FREETYPE_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/freetype/include) message(STATUS "Using in-tree pixman") - add_vendored_subdirectory(extlib/pixman) set(PIXMAN_FOUND YES) set(PIXMAN_LIBRARY pixman) + set(PIXMAN_BUILD_TESTS OFF CACHE BOOL "") + set(PIXMAN_BUILD_DEMOS OFF CACHE BOOL "") + set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) + add_vendored_subdirectory(extlib/pixman) message(STATUS "Using in-tree cairo") add_vendored_subdirectory(extlib/cairo) @@ -275,6 +274,7 @@ if(ENABLE_GUI) elseif(APPLE) find_package(OpenGL REQUIRED) find_library(APPKIT_LIBRARY AppKit REQUIRED) + set(util_LIBRARIES ${APPKIT_LIBRARY}) else() find_package(OpenGL REQUIRED) find_package(SpaceWare) diff --git a/cmake/libpng-macos-arm64.patch b/cmake/libpng-macos-arm64.patch new file mode 100644 index 00000000..2d0e15cf --- /dev/null +++ b/cmake/libpng-macos-arm64.patch @@ -0,0 +1,117 @@ +diff --git a/extlib/libpng/CMakeLists.txt b/extlib/libpng/CMakeLists.txt +index 42ff0f9025..6834ea332e 100644 +--- a/extlib/libpng/CMakeLists.txt ++++ b/extlib/libpng/CMakeLists.txt +@@ -65,11 +65,22 @@ option(PNG_HARDWARE_OPTIMIZATIONS "Enable hardware optimizations" ON) + set(PNG_PREFIX "" CACHE STRING "Prefix to add to the API function names") + set(DFA_XTRA "" CACHE FILEPATH "File containing extra configuration settings") + ++# CMake currently sets CMAKE_SYSTEM_PROCESSOR to one of x86_64 or arm64 on macOS, ++# based upon the OS architecture, not the target architecture. As such, we need ++# to check CMAKE_OSX_ARCHITECTURES to identify which hardware-specific flags to ++# enable. Note that this will fail if you attempt to build a universal binary in ++# a single cmake invocation. ++if (APPLE AND CMAKE_OSX_ARCHITECTURES) ++ set(TARGET_ARCH ${CMAKE_OSX_ARCHITECTURES}) ++else() ++ set(TARGET_ARCH ${CMAKE_SYSTEM_PROCESSOR}) ++endif() ++ + if(PNG_HARDWARE_OPTIMIZATIONS) + + # Set definitions and sources for ARM. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "^arm" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "^aarch64") ++if(TARGET_ARCH MATCHES "^arm" OR ++ TARGET_ARCH MATCHES "^aarch64") + set(PNG_ARM_NEON_POSSIBLE_VALUES check on off) + set(PNG_ARM_NEON "check" + CACHE STRING "Enable ARM NEON optimizations: check|on|off; check is default") +@@ -95,8 +106,8 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^arm" OR + endif() + + # Set definitions and sources for PowerPC. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "^powerpc*" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "^ppc64*") ++if(TARGET_ARCH MATCHES "^powerpc*" OR ++ TARGET_ARCH MATCHES "^ppc64*") + set(PNG_POWERPC_VSX_POSSIBLE_VALUES on off) + set(PNG_POWERPC_VSX "on" + CACHE STRING "Enable POWERPC VSX optimizations: on|off; on is default") +@@ -118,8 +129,8 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^powerpc*" OR + endif() + + # Set definitions and sources for Intel. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "^i?86" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "^x86_64*") ++if(TARGET_ARCH MATCHES "^i?86" OR ++ TARGET_ARCH MATCHES "^x86_64*") + set(PNG_INTEL_SSE_POSSIBLE_VALUES on off) + set(PNG_INTEL_SSE "on" + CACHE STRING "Enable INTEL_SSE optimizations: on|off; on is default") +@@ -141,8 +152,8 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^i?86" OR + endif() + + # Set definitions and sources for MIPS. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "mipsel*" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "mips64el*") ++if(TARGET_ARCH MATCHES "mipsel*" OR ++ TARGET_ARCH MATCHES "mips64el*") + set(PNG_MIPS_MSA_POSSIBLE_VALUES on off) + set(PNG_MIPS_MSA "on" + CACHE STRING "Enable MIPS_MSA optimizations: on|off; on is default") +@@ -166,26 +177,26 @@ endif() + else(PNG_HARDWARE_OPTIMIZATIONS) + + # Set definitions and sources for ARM. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "^arm" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "^aarch64") ++if(TARGET_ARCH MATCHES "^arm" OR ++ TARGET_ARCH MATCHES "^aarch64") + add_definitions(-DPNG_ARM_NEON_OPT=0) + endif() + + # Set definitions and sources for PowerPC. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "^powerpc*" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "^ppc64*") ++if(TARGET_ARCH MATCHES "^powerpc*" OR ++ TARGET_ARCH MATCHES "^ppc64*") + add_definitions(-DPNG_POWERPC_VSX_OPT=0) + endif() + + # Set definitions and sources for Intel. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "^i?86" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "^x86_64*") ++if(TARGET_ARCH MATCHES "^i?86" OR ++ TARGET_ARCH MATCHES "^x86_64*") + add_definitions(-DPNG_INTEL_SSE_OPT=0) + endif() + + # Set definitions and sources for MIPS. +-if(CMAKE_SYSTEM_PROCESSOR MATCHES "mipsel*" OR +- CMAKE_SYSTEM_PROCESSOR MATCHES "mips64el*") ++if(TARGET_ARCH MATCHES "mipsel*" OR ++ TARGET_ARCH MATCHES "mips64el*") + add_definitions(-DPNG_MIPS_MSA_OPT=0) + endif() + +@@ -412,19 +412,11 @@ else() + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/scripts/checksym.awk" + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/symbols.def") + +- add_custom_target(symbol-check +- DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/scripts/symbols.chk") +- + generate_copy("${CMAKE_CURRENT_BINARY_DIR}/scripts/sym.out" + "${CMAKE_CURRENT_BINARY_DIR}/libpng.sym") + generate_copy("${CMAKE_CURRENT_BINARY_DIR}/scripts/vers.out" + "${CMAKE_CURRENT_BINARY_DIR}/libpng.vers") + +- add_custom_target(genvers +- DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/libpng.vers") +- add_custom_target(gensym +- DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/libpng.sym") +- + add_custom_target("genprebuilt" + COMMAND "${CMAKE_COMMAND}" + "-DOUTPUT=scripts/pnglibconf.h.prebuilt" diff --git a/extlib/libpng b/extlib/libpng index e9c3d83d..dbe3e0c4 160000 --- a/extlib/libpng +++ b/extlib/libpng @@ -1 +1 @@ -Subproject commit e9c3d83d5a04835806287f1e8c0f2d3a962d6673 +Subproject commit dbe3e0c43e549a1602286144d94b0666549b18e6 diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index 5b7c45d1..0f737a1e 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -31,7 +31,7 @@ if(WIN32) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${source}") endfunction() elseif(APPLE) - set(app_resource_dir ${CMAKE_BINARY_DIR}/bin/SolveSpace.app/Contents/Resources) + set(app_resource_dir ${CMAKE_BINARY_DIR}/Resources) set(cli_resource_dir ${CMAKE_BINARY_DIR}/res) function(add_resource name) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45dab944..5ac7b41d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -338,7 +338,10 @@ if(ENABLE_GUI) LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF") elseif(APPLE) set_target_properties(solvespace PROPERTIES - OUTPUT_NAME SolveSpace) + OUTPUT_NAME SolveSpace + XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES" + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() endif() @@ -401,17 +404,30 @@ endif() # solvespace macOS package if(APPLE) - set(bundle SolveSpace) - set(bundle_bin ${EXECUTABLE_OUTPUT_PATH}/${bundle}.app/Contents/MacOS) - set(bundle_resources ${EXECUTABLE_OUTPUT_PATH}/${bundle}.app/Contents/Resources/lib) - execute_process( - COMMAND mkdir -p ${bundle_resources} - COMMAND cp -p /usr/local/opt/libomp/lib/libomp.dylib ${bundle_resources}/libomp.dylib - ) - add_custom_command(TARGET solvespace POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${bundle_bin} - COMMAND ${CMAKE_COMMAND} -E copy $ ${bundle_bin} - COMMAND install_name_tool -change /usr/local/opt/libomp/lib/libomp.dylib "@executable_path/../Resources/lib/libomp.dylib" ${bundle_bin}/${bundle} - COMMENT "Bundling executable solvespace-cli" - VERBATIM) -endif() + set(LIBOMP_LIB_PATH ${OpenMP_CXX_INCLUDE_DIRS}/../lib/libomp.dylib) + set(LIBOMP_LINK_PATH "@executable_path/../Resources/libomp.dylib") + set(LIBOMP_LINK_PATH_UTILS "@executable_path/SolveSpace.app/Contents/Resources/libomp.dylib") + if(ENABLE_GUI) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND cp -r ${CMAKE_BINARY_DIR}/Resources $ + ) + if(ENABLE_OPENMP) + execute_process(COMMAND install_name_tool -id ${LIBOMP_LINK_PATH} ${LIBOMP_LIB_PATH}) + message("FROM " ${${LIBOMP_LIB_PATH}} "TO" $/Resources/libomp.dylib) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${LIBOMP_LIB_PATH} $/Resources/libomp.dylib + COMMAND install_name_tool -change ${LIBOMP_LINK_PATH} ${LIBOMP_LINK_PATH_UTILS} $ + ) + endif() + endif() + if(ENABLE_TESTS AND ENABLE_OPENMP) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND install_name_tool -change ${LIBOMP_LINK_PATH} ${LIBOMP_LINK_PATH_UTILS} $) + endif() + if(ENABLE_CLI) + add_custom_command(TARGET solvespace POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ $ + COMMENT "Bundling executable solvespace-cli" + VERBATIM) + endif() +endif() \ No newline at end of file