diff --git a/lib/releaseme/assert_case_insensitive.rb b/lib/releaseme/assert_case_insensitive.rb new file mode 100644 index 0000000..e28d236 --- /dev/null +++ b/lib/releaseme/assert_case_insensitive.rb @@ -0,0 +1,64 @@ +#-- +# Copyright (C) 2017 Harald Sitter +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License or (at your option) version 3 or any later version +# accepted by the membership of KDE e.V. (or its successor approved +# by the membership of KDE e.V.), which shall act as a proxy +# defined in Section 14 of version 3 of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#++ + +require 'json' + +require_relative 'logable' + +module ReleaseMe + class AssertionFailedError < StandardError; end + + # Asserts that a file tree does not contain case-conflicting files. + module AssertCaseInsensitive + prepend Logable + + class << self + # rubocop:disable Metrics/MethodLength + def assert(dir) + # This obviously suffers from a bit of a flaw in that if releaseme + # gets run on a case-insensitive FS to begin with the conflicts may + # result in overwrites in other code (notably l10n fetching). + # Seeing as most devs run Linux that is unlikely to happen right now, + # but may need rectifying at some point by making all code globally + # butt out if files were to be overwritten. + # entries = Dir.glob("#{dir}/**/**").collect(&:downcase) + entries = Dir.glob("#{dir}/**/**").group_by(&:downcase) + dupes = entries.select { |_, canonical_paths| canonical_paths.size > 1 } + return if dupes.nil? || dupes.empty? + log_fatal <<-ERRORMSG +\n +The resulting tarball contains case-sensitive conflicting files. This makes the +tarball incompatible with case-insensitive file systems or operating systems +(e.g. Windows). This is a fatal problem and must be solved before release! +To resolve the problem the case-conflicting files and/or directories need to be +renamed so that their fully downcased representations do not conflict anymore. + +e.g. src/Foo.txt and Src/foo.txt are both src/foo.txt when fully converted to + lower case. To resolve the issue you could rename the directory Src to bar + so you get src/Foo.txt and bar/foo.txt and they no longer conflict + +The conflicting paths are: +#{JSON.pretty_generate(dupes)} + ERRORMSG + raise AssertionFailedError + end + end + end +end diff --git a/lib/releaseme/xzarchive.rb b/lib/releaseme/xzarchive.rb index d9f2a3a..49902f9 100644 --- a/lib/releaseme/xzarchive.rb +++ b/lib/releaseme/xzarchive.rb @@ -1,87 +1,94 @@ #-- # Copyright (C) 2007-2017 Harald Sitter # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License or (at your option) version 3 or any later version # accepted by the membership of KDE e.V. (or its successor approved # by the membership of KDE e.V.), which shall act as a proxy # defined in Section 14 of version 3 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . #++ +require_relative 'assert_case_insensitive' + module ReleaseMe ## # Tar-XZ Archiving Class. # This class archives @directory into an tar file and compresses it using xz. # Compression strength is set via @level. class XzArchive # The directory to archive attr_accessor :directory # XZ compression level (must be between 1 and 9 - other values will not # result in an archive file) attr_accessor :level # XZ compressed tarball file name (e.g. foobar-1.tar.xz) # This is nil unless create() finished successfully. attr_reader :filename # @return String absolute path of archive file attr_reader :path LEVEL_RANGE = 0..9 # Creates new XzArchive. @directory must be assigned separately. def initialize @directory = nil @level = 9 @filename = nil @path = nil end ## # call-seq: # archive.create() -> true or false # # Create the archive. Creates an archive based on the directory attribute. # Results in @directory.tar.xz in the present working directory. #-- # FIXME: need routine to run and log a) command b) results c) outputs #++ def create xz = "#{directory}.tar.xz" return false unless valid? FileUtils.rm_rf(xz) # Note that system returns bool but only captures stdout. compress(directory, xz) || raise return true rescue RuntimeError FileUtils.rm_rf(xz) return false end private def valid? - File.exist?(@directory) && LEVEL_RANGE.include?(@level) + File.exist?(@directory) && LEVEL_RANGE.include?(@level) && asserts + end + + def asserts + AssertCaseInsensitive.assert(@directory) + true # Included in && chain. end def compress(dir, xz) # Tar and compress in one go. tar supports -J for quite a while now. system({ 'XZ_OPT' => "-#{level}" }, 'tar', 'cfJ', xz, dir, %i[out err] => '/dev/null') @filename = xz @path = File.realpath(xz) end end end diff --git a/test/assert_case_insensitive_test.rb b/test/assert_case_insensitive_test.rb new file mode 100644 index 0000000..f2fa3e3 --- /dev/null +++ b/test/assert_case_insensitive_test.rb @@ -0,0 +1,68 @@ +#-- +# Copyright (C) 2017 Harald Sitter +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License or (at your option) version 3 or any later version +# accepted by the membership of KDE e.V. (or its successor approved +# by the membership of KDE e.V.), which shall act as a proxy +# defined in Section 14 of version 3 of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#++ + +require 'fileutils' + +require_relative 'lib/testme' +require_relative '../lib/releaseme/assert_case_insensitive' + +class TestAssertCaseInsensitive < Testme + def setup + @dir = 'sourcy' + teardown # Make sure everything is clean... + Dir.mkdir(@dir) + end + + def teardown + FileUtils.rm_rf(@dir) + end + + def test_case_sensitive + a = "#{@dir}/foo.txt" + b = "#{@dir}/Foo.txt" + + File.write(a, 'a') + File.write(b, 'b') + + # Only works on case sensitive file systems obviously + unless File.read(a) == 'a' && File.read(b) == 'b' + return # Which the current one is not! + end + + File.write("#{@dir}/bar.txt", '') + + assert_raises ReleaseMe::AssertionFailedError do + ReleaseMe::AssertCaseInsensitive.assert(@dir) + end + end + + def test_case_insensitive + # i.e. no conflict; passes assertion + + FileUtils.mkpath("#{@dir}/subdir1") + FileUtils.mkpath("#{@dir}/subdir2") + File.write("#{@dir}/foo.txt", 'a') + File.write("#{@dir}/bar.txt", 'b') + File.write("#{@dir}/subdir1/foo.txt", 'c') + File.write("#{@dir}/subdir2/foo.txt", 'd') + + ReleaseMe::AssertCaseInsensitive.assert(@dir) + end +end