From 3463d6740a5ad48e7b9ce3f89886639136c2e840 Mon Sep 17 00:00:00 2001 From: donghee Date: Tue, 18 Sep 2012 17:05:19 +0900 Subject: [PATCH] Update Tizen 2.0 SDK source code Change-Id: I8625ecad315e7cb9a07e52cfff0bc594eda4f4f0 --- README | 4 +- build-cli | 379 +++++- build-svr | 125 +- doc/DIBS_Advanced_Guide.pdf | Bin 0 -> 290553 bytes doc/Tizen_SDK_Development_Guide.pdf | Bin 0 -> 90258 bytes doc/Tizen_SDK_Package_Guide.pdf | Bin 0 -> 22175 bytes package/build.linux | 13 +- package/build.macos | 31 + package/build.windows | 31 + package/pkginfo.manifest | 15 +- package/pkginfo.manifest.local | 6 + pkg-build | 28 +- pkg-clean | 6 + pkg-cli | 80 +- pkg-svr | 39 +- src/build_server/BinaryUploadProject.rb | 91 ++ src/build_server/BuildClientOptionParser.rb | 159 ++- src/build_server/BuildComm.rb | 447 ++++++- src/build_server/BuildJob.rb | 1183 ++++++++++++++--- src/build_server/BuildServer.rb | 216 ++- src/build_server/BuildServerController.rb | 388 +++++- src/build_server/BuildServerOptionParser.rb | 180 ++- src/build_server/CommonProject.rb | 102 ++ src/build_server/FullBuildJob.rb | 255 ++++ src/build_server/GitBuildJob.rb | 203 ++- src/build_server/GitBuildProject.rb | 264 ++++ src/build_server/JobClean.rb | 192 +++ src/build_server/JobLog.rb | 76 +- src/build_server/JobManager.rb | 417 ++++-- src/build_server/LocalBuildJob.rb | 148 --- src/build_server/MultiBuildJob.rb | 489 +++++++ src/build_server/PackageSync.rb | 187 +++ src/build_server/ProjectManager.rb | 352 +++++ src/build_server/RegisterPackageJob.rb | 544 ++++++++ src/build_server/RemoteBuildJob.rb | 7 +- src/build_server/RemoteBuildServer.rb | 35 +- src/build_server/RemoteBuilder.rb | 210 +++ src/build_server/ReverseBuildChecker.rb | 218 +++ src/build_server/SocketJobRequestListener.rb | 744 ++++++++++- src/builder/Builder.rb | 650 +++++---- src/builder/CleanOptionParser.rb | 16 +- src/builder/optionparser.rb | 38 +- src/common/Action.rb | 47 + src/common/PackageManifest.rb | 83 +- src/common/ScheduledActionHandler.rb | 99 ++ src/common/Version.rb | 5 + src/common/fileTransfer.rb | 118 ++ src/common/log.rb | 42 +- src/common/package.rb | 141 +- src/common/parser.rb | 452 ++++--- src/common/utils.rb | 400 +++++- src/pkg_server/DistSync.rb | 98 ++ src/pkg_server/SocketRegisterListener.rb | 213 +++ src/pkg_server/client.rb | 1083 +++++++++------ src/pkg_server/clientOptParser.rb | 163 +-- src/pkg_server/distribution.rb | 1019 +++++++++----- src/pkg_server/downloader.rb | 27 +- src/pkg_server/installer.rb | 329 +++-- src/pkg_server/packageServer.rb | 806 ++++++----- src/pkg_server/packageServerConfig.rb | 2 - src/pkg_server/serverOptParser.rb | 219 +-- test/a/a | 1 - test/a/package/build.linux | 35 - test/a/package/pkginfo.manifest | 7 - test/b/b | 1 - test/b/package/build.linux | 27 - test/b/package/pkginfo.manifest | 7 - test/bin/bin_0.0.0_linux.zip | Bin 0 -> 620 bytes test/bin/bin_0.0.0_ubuntu-32.zip | Bin 0 -> 624 bytes test/bin/bin_0.0.1_linux.zip | Bin 0 -> 622 bytes test/bin/bin_0.0.1_ubuntu-32.zip | Bin 0 -> 626 bytes test/bin/src.tar.gz | Bin 0 -> 150 bytes test/build-cli-01.testcase | 46 +- test/build-cli-02.testcase | 37 +- test/build-cli-03.testcase | 38 +- test/build-cli-03_1.testcase | 28 + test/build-cli-04.testcase | 7 + test/build-cli-05.testcase | 6 + test/build-cli-06.testcase | 6 + test/build-cli-07.testcase | 11 + test/build-cli-08.testcase | 32 + test/build-cli-09.testcase | 19 + test/build-cli-10.testcase | 32 + test/build-cli-11.testcase | 11 + test/build-cli-12.testcase | 11 + test/build-cli-12_1.testcase | 10 + test/build-cli-13.testcase | 38 + test/build-cli-14.testcase | 9 + test/build-cli-15.testcase | 9 + test/build-cli-16.testcase | 8 + test/build-cli-17.testcase | 40 + test/build-cli-18.testcase | 31 + test/build-cli-19.testcase | 16 + test/build-cli-20.testcase | 6 + test/build-cli-21.testcase | 6 + test/build-cli-22.testcase | 16 + test/build-cli-23.testcase | 25 + test/build-cli-24.testcase | 12 + test/build-cli-25.testcase | 20 + test/build-cli-26.testcase | 45 + test/build-cli-27.testcase | 7 + test/build-cli-28.testcase | 33 + test/build-cli-29.testcase | 38 + test/build-svr-01.testcase | 11 + test/build-svr-02.testcase | 45 + ...erver03.testcase => build-svr-03.testcase} | 4 +- ...erver04.testcase => build-svr-04.testcase} | 3 +- ...erver05.testcase => build-svr-05.testcase} | 6 +- ...erver06.testcase => build-svr-06.testcase} | 2 +- test/build-svr-07.testcase | 9 + test/build-svr-08.testcase | 12 + test/build-svr-09.testcase | 6 + test/build-svr-10.testcase | 6 + test/build-svr-11.testcase | 12 + test/build-svr-12.testcase | 6 + test/build-svr-13.testcase | 11 + test/build-svr-14.testcase | 12 + test/build-svr-15.testcase | 19 + test/build-svr-16.testcase | 15 + test/build-svr-17.testcase | 13 + test/build-svr-18.testcase | 13 + test/build-svr-19.testcase | 16 + test/build-svr-20.testcase | 20 + test/buildcli.testsuite | 28 + test/buildserver.testsuite | 24 +- test/buildserver01.testcase | 11 - test/buildserver02.testcase | 18 - test/buildsvr.init | 32 + test/c/c | 1 - test/c/package/build.linux | 21 - test/c/package/pkginfo.manifest | 6 - test/git01/a.tar.gz | Bin 0 -> 14112 bytes test/git01/a1_v1.tar.gz | Bin 0 -> 9123 bytes test/git01/a_new.tar.gz | Bin 0 -> 16165 bytes test/git01/a_v1.tar.gz | Bin 0 -> 8555 bytes test/git01/a_v2.tar.gz | Bin 0 -> 9397 bytes test/git01/a_v3.tar.gz | Bin 0 -> 10099 bytes test/git01/a_v4.tar.gz | Bin 0 -> 10958 bytes test/git01/a_v5.tar.gz | Bin 0 -> 11639 bytes test/git01/b.tar.gz | Bin 0 -> 13951 bytes test/git01/b_new.tar.gz | Bin 0 -> 15255 bytes test/git01/b_v1.tar.gz | Bin 0 -> 8757 bytes test/git01/b_v2.tar.gz | Bin 0 -> 10258 bytes test/git01/b_v4.tar.gz | Bin 0 -> 11469 bytes test/git01/c.tar.gz | Bin 0 -> 18834 bytes test/git01/c_new.tar.gz | Bin 0 -> 19975 bytes test/git01/c_v1.tar.gz | Bin 0 -> 9107 bytes test/git01/c_v1_1.tar.gz | Bin 0 -> 9680 bytes test/git01/c_v2.tar.gz | Bin 0 -> 10857 bytes test/git01/c_v4.tar.gz | Bin 0 -> 11979 bytes test/git01/c_v5.tar.gz | Bin 0 -> 12861 bytes test/git01/d.tar.gz | Bin 0 -> 15469 bytes test/git01/d_v0.tar.gz | Bin 0 -> 17463 bytes test/packageserver.testsuite | 3 +- test/packageserver01.testcase | 84 +- test/packageserver02.testcase | 3 +- test/packageserver03.testcase | 4 +- test/packageserver04.testcase | 3 +- test/packageserver05.testcase | 2 +- test/packageserver06.testcase | 4 +- test/packageserver07.testcase | 4 +- test/packageserver08.testcase | 2 +- test/packageserver09.testcase | 2 +- test/packageserver10.testcase | 2 +- test/packageserver11.testcase | 2 +- test/packageserver12.testcase | 2 +- test/packageserver13.testcase | 2 +- test/packageserver14.testcase | 2 +- test/packageserver15.testcase | 2 +- test/packageserver16.testcase | 2 +- test/packageserver17.testcase | 2 +- test/packageserver19.testcase | 2 +- test/packageserver20.testcase | 2 +- test/packageserver21.testcase | 2 +- test/packageserver22.testcase | 2 +- test/packageserver23.testcase | 2 +- test/packageserver24.testcase | 8 + test/packageserver25.testcase | 6 + test/pkg-cli-download.testcase | 2 +- test/pkg-cli-listrpkg.testcase | 2 +- test/pkg-cli-showrpkg.testcase | 2 +- test/pkg-list | 17 +- test/pkg-list-local | 17 + test/pkgsvr.init | 7 + test/pkgsvr2.init | 7 + test/regression.rb | 51 +- test/test_bserver2c.rb | 25 - test/test_bserver3c.rb | 28 - test/test_pkglist_parser.rb | 7 +- test/test_server | 69 +- test/test_server_pkg_file/archive.zip | Bin 0 -> 308 bytes .../smart-build-interface_1.20.1.tar.gz | Bin 78 -> 0 bytes .../smart-build-interface_1.20.1_linux.zip | Bin 327 -> 308 bytes tizen-ide/get_ide_sources.sh | 38 +- upgrade | 256 ++++ 195 files changed, 12435 insertions(+), 3271 deletions(-) create mode 100644 doc/DIBS_Advanced_Guide.pdf create mode 100644 doc/Tizen_SDK_Development_Guide.pdf create mode 100644 doc/Tizen_SDK_Package_Guide.pdf create mode 100644 package/build.macos create mode 100644 package/build.windows create mode 100644 package/pkginfo.manifest.local create mode 100644 src/build_server/BinaryUploadProject.rb create mode 100644 src/build_server/CommonProject.rb create mode 100644 src/build_server/FullBuildJob.rb create mode 100644 src/build_server/GitBuildProject.rb create mode 100644 src/build_server/JobClean.rb delete mode 100644 src/build_server/LocalBuildJob.rb create mode 100644 src/build_server/MultiBuildJob.rb create mode 100644 src/build_server/PackageSync.rb create mode 100644 src/build_server/ProjectManager.rb create mode 100644 src/build_server/RegisterPackageJob.rb create mode 100644 src/build_server/RemoteBuilder.rb create mode 100644 src/build_server/ReverseBuildChecker.rb create mode 100644 src/common/Action.rb create mode 100644 src/common/ScheduledActionHandler.rb create mode 100644 src/common/fileTransfer.rb create mode 100644 src/pkg_server/DistSync.rb create mode 100644 src/pkg_server/SocketRegisterListener.rb delete mode 100644 test/a/a delete mode 100755 test/a/package/build.linux delete mode 100644 test/a/package/pkginfo.manifest delete mode 100644 test/b/b delete mode 100755 test/b/package/build.linux delete mode 100644 test/b/package/pkginfo.manifest create mode 100644 test/bin/bin_0.0.0_linux.zip create mode 100644 test/bin/bin_0.0.0_ubuntu-32.zip create mode 100644 test/bin/bin_0.0.1_linux.zip create mode 100644 test/bin/bin_0.0.1_ubuntu-32.zip create mode 100644 test/bin/src.tar.gz create mode 100644 test/build-cli-03_1.testcase create mode 100644 test/build-cli-04.testcase create mode 100644 test/build-cli-05.testcase create mode 100644 test/build-cli-06.testcase create mode 100644 test/build-cli-07.testcase create mode 100644 test/build-cli-08.testcase create mode 100644 test/build-cli-09.testcase create mode 100644 test/build-cli-10.testcase create mode 100644 test/build-cli-11.testcase create mode 100644 test/build-cli-12.testcase create mode 100644 test/build-cli-12_1.testcase create mode 100644 test/build-cli-13.testcase create mode 100644 test/build-cli-14.testcase create mode 100644 test/build-cli-15.testcase create mode 100644 test/build-cli-16.testcase create mode 100644 test/build-cli-17.testcase create mode 100644 test/build-cli-18.testcase create mode 100644 test/build-cli-19.testcase create mode 100644 test/build-cli-20.testcase create mode 100644 test/build-cli-21.testcase create mode 100644 test/build-cli-22.testcase create mode 100644 test/build-cli-23.testcase create mode 100644 test/build-cli-24.testcase create mode 100644 test/build-cli-25.testcase create mode 100644 test/build-cli-26.testcase create mode 100644 test/build-cli-27.testcase create mode 100644 test/build-cli-28.testcase create mode 100644 test/build-cli-29.testcase create mode 100644 test/build-svr-01.testcase create mode 100644 test/build-svr-02.testcase rename test/{buildserver03.testcase => build-svr-03.testcase} (53%) rename test/{buildserver04.testcase => build-svr-04.testcase} (58%) rename test/{buildserver05.testcase => build-svr-05.testcase} (51%) rename test/{buildserver06.testcase => build-svr-06.testcase} (53%) create mode 100644 test/build-svr-07.testcase create mode 100644 test/build-svr-08.testcase create mode 100644 test/build-svr-09.testcase create mode 100644 test/build-svr-10.testcase create mode 100644 test/build-svr-11.testcase create mode 100644 test/build-svr-12.testcase create mode 100644 test/build-svr-13.testcase create mode 100644 test/build-svr-14.testcase create mode 100644 test/build-svr-15.testcase create mode 100644 test/build-svr-16.testcase create mode 100644 test/build-svr-17.testcase create mode 100644 test/build-svr-18.testcase create mode 100644 test/build-svr-19.testcase create mode 100644 test/build-svr-20.testcase delete mode 100644 test/buildserver01.testcase delete mode 100644 test/buildserver02.testcase create mode 100644 test/buildsvr.init delete mode 100644 test/c/c delete mode 100755 test/c/package/build.linux delete mode 100644 test/c/package/pkginfo.manifest create mode 100644 test/git01/a.tar.gz create mode 100644 test/git01/a1_v1.tar.gz create mode 100644 test/git01/a_new.tar.gz create mode 100644 test/git01/a_v1.tar.gz create mode 100644 test/git01/a_v2.tar.gz create mode 100644 test/git01/a_v3.tar.gz create mode 100644 test/git01/a_v4.tar.gz create mode 100644 test/git01/a_v5.tar.gz create mode 100644 test/git01/b.tar.gz create mode 100644 test/git01/b_new.tar.gz create mode 100644 test/git01/b_v1.tar.gz create mode 100644 test/git01/b_v2.tar.gz create mode 100644 test/git01/b_v4.tar.gz create mode 100644 test/git01/c.tar.gz create mode 100644 test/git01/c_new.tar.gz create mode 100644 test/git01/c_v1.tar.gz create mode 100644 test/git01/c_v1_1.tar.gz create mode 100644 test/git01/c_v2.tar.gz create mode 100644 test/git01/c_v4.tar.gz create mode 100644 test/git01/c_v5.tar.gz create mode 100644 test/git01/d.tar.gz create mode 100644 test/git01/d_v0.tar.gz create mode 100644 test/packageserver24.testcase create mode 100644 test/packageserver25.testcase create mode 100644 test/pkg-list-local create mode 100644 test/pkgsvr.init create mode 100644 test/pkgsvr2.init delete mode 100755 test/test_bserver2c.rb delete mode 100755 test/test_bserver3c.rb create mode 100644 test/test_server_pkg_file/archive.zip delete mode 100644 test/test_server_pkg_file/smart-build-interface_1.20.1.tar.gz create mode 100644 upgrade diff --git a/README b/README index e54ec3f..f373da2 100644 --- a/README +++ b/README @@ -142,7 +142,7 @@ Building a SDK package is very simple. Here is the command for buiding package. ## pkg-build [-u ] [-o ] [-c ] [-r ] ## -u : Package server URL which contains binary and development packages. ## If ommited, it will use previous server URL. - ## -o : Target OS(linux or windows) + ## -o : Target OS(ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64) ## -c : Clean build"" ## If set, start build after downloading all dependent packages ## If not set, it will not download dependent packages if already downloaded @@ -195,7 +195,7 @@ There are more useful commands provided You can list up available packages of server. ## pkg-cli list-rpkg [-o ] [-u ] - ## -o : Target OS(linux or windows) + ## -o : Target OS(ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64) ## -u : Package server URL which contains binary and development packages. ## If ommited, it will use previous server URL. diff --git a/build-cli b/build-cli index 4bec172..8207598 100755 --- a/build-cli +++ b/build-cli @@ -1,4 +1,4 @@ -#!/usr/bin/ruby -d +#!/usr/bin/ruby =begin @@ -36,75 +36,370 @@ require "utils" require "BuildClientOptionParser" require "BuildComm" + + #option parsing -option = option_parse +begin + option = option_parse +rescue => e + puts e.message + exit 0 +end + + +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end + +def query( ip, port, sym ) + client = BuildCommClient.create( ip, port, nil, 0 ) + if client.nil? then + puts "Connection to server failed!" + return nil + end + client.send "QUERY|#{sym.strip}" + result = client.receive_data() + client.terminate + return result +end + +def query_system_info(ip, port) + # HOST SYSTEM INFO + puts "* SYSTEM INFO *" + data = query( ip, port, "SYSTEM") + if data.nil? then exit 1 end + + result = data[0].split(",").map { |x| x.strip } + puts "HOST-OS: #{result[0]}" + puts "MAX_WORKING_JOBS: #{result[1]}" + + # FTP INFO + puts "\n* FTP *" + data = query(ip, port, "FTP") + if data.nil? then exit 1 end + + result = data[0].split(",").map { |x| x.strip } + puts "FTP_ADDR: #{result[0]}" + puts "FTP_USERNAME: #{result[1]}" + + # SUPPORTED OS INFO + puts "\n* SUPPORTED OS LIST *" + data = query(ip, port, "OS") + if data.nil? then exit 1 end + + data.each do |item| + puts "#{item.strip}" + end + + # Friend lists + puts "\n* FRIEND SERVER LIST (WAIT|WORK/MAX) jobs [transfer count] *" + data = query(ip, port, "FRIEND") + if data.nil? then exit 1 end + i = 0 + data.each do |item| + i = i + 1 + info = item.split(",").map { |x| x.strip } + if info[0] == "DISCONNECTED" then + puts "#{i}. #{info[0]}" + else + puts "#{i}. #{info[0]} #{info[1]} server (#{info[2]}|#{info[3]}/#{info[4]}) [#{info[5]}]" + end + end +end + + +def query_project_list(ip, port) + puts "* PROJECT(S) *" + data = query( ip, port, "PROJECT") + data.each do |item| + tok = item.split(",").map { |x| x.strip } + type = (tok[0]=="G" ? "NORMAL":"REMOTE") + printf("%-25s %s\n",tok[1],type) + end +end -# if "--os" is not specified, use host os type + +def query_job_list(ip, port) + puts "* JOB(S) *" + data = query(ip, port, "JOB") + data.each do |item| + tok = item.split(",").map { |x| x.strip } + if tok[3].nil? then + puts "#{tok[1]} #{tok[0]} #{tok[2]}" + else + puts "#{tok[1]} #{tok[0]} #{tok[2]} (#{tok[3]})" + end + end +end + + +# if "--os" is not specified, use pe if option[:os].nil? then - option[:os] = Utils::HOST_OS -else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end + option[:os] = "default" end if option[:domain].nil? then + puts "Warn: Build server IP address is not specified. 127.0.0.1 will be used" option[:domain] = "127.0.0.1" end -if option[:port].nil? then - option[:port] = 2222 -end - begin case option[:cmd] when "build" - client = BuildCommClient.create( option[:domain], option[:port]) + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + client = BuildCommClient.create( result[0], result[1], nil, 0 ) if not client.nil? then - client.send "BUILD,GIT,#{option[:git]},#{option[:commit]},#{option[:os]},,#{option[:async]}" + client.send "BUILD|GIT|#{option[:project]}|#{option[:passwd]}|#{option[:os]}|#{option[:async]}|#{option[:noreverse]}" client.print_stream client.terminate + else + puts "Connection to server failed!" + exit 1 end when "resolve" - client = BuildCommClient.create( option[:domain], option[:port]) + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + client = BuildCommClient.create( result[0], result[1], nil, 0 ) if not client.nil? then - client.send "RESOLVE,GIT,#{option[:git]},#{option[:commit]},#{option[:os]},,#{option[:async]}" + client.send "RESOLVE|GIT|#{option[:project]}|#{option[:passwd]}|#{option[:os]}|#{option[:async]}" client.print_stream client.terminate end when "query" - # SYSTEM INFO - client = BuildCommClient.create( option[:domain], option[:port]) - if not client.nil? then - client.send "QUERY,SYSTEM" - result0 = client.receive_data() - if result0.nil? then - client.terminate - exit(-1) + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_system_info( result[0], result[1] ) + puts "" + query_project_list( result[0], result[1]) + puts "" + query_job_list( result[0], result[1]) + + when "query-system" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_system_info( result[0], result[1] ) + + when "query-project" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_project_list( result[0], result[1]) + + when "query-job" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_job_list( result[0], result[1] ) + + when "cancel" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + if not option[:job].nil? then + client = BuildCommClient.create( result[0], result[1], nil, 0 ) + if not client.nil? then + client.send "CANCEL|#{option[:job]}|#{option[:passwd]}" + result1 = client.receive_data() + if result1.nil? then + client.terminate + exit(-1) + end + puts result1 + else + puts "Connection to server failed!" + exit 1 end - result0 = result0[0].split(",").map { |x| x.strip } - puts "HOST-OS: #{result0[0]}" - puts "MAX_WORKING_JOBS: #{result0[1]}" + else + puts "you must input \"cancel job number\"!!" + exit 1 + end + + when "register" + # check file exist + if not File.exist? option[:package] then + puts "The file does not exist!.. #{option[:package]}" + exit(-1) + end + + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + bs_ip = result[0] + bs_port = result[1] + + ftp_result = Utils.parse_ftpserver_url(option[:fdomain]) + if ftp_result.nil? or ftp_result.length != 4 then + puts "FTP server url is incorrect. (#{option[:fdomain]})" + puts "Tune as following format." + puts " ftp://:@
" + exit 1 + end + ip = ftp_result[0] + port = ftp_result[1] + username = ftp_result[2] + passwd = ftp_result[3] + + # upload + client = BuildCommClient.create( bs_ip, bs_port, nil, 0 ) + if client.nil? then + puts "Can't access server #{bs_ip}:#{bs_port}" + exit(-1) + end + dock = Utils.create_uniq_name() + msg = "UPLOAD|#{dock}" + client.send( msg ) + result = client.send_file(ip, port, username, passwd, option[:package]) + client.terminate + if not result then + puts "Uploading file failed!.. #{option[:package]}" + exit(-1) + end + + # register + client = BuildCommClient.create( bs_ip, bs_port, nil, 0 ) + if client.nil? then + puts "Can't access server #{bs_ip}:#{bs_port}" + exit(-1) + end + client.send("REGISTER|BINARY|#{File.basename(option[:package])}|#{option[:passwd]}|#{dock}") + client.print_stream + client.terminate + + # for test + when "upload" + # check file exist + if not File.exist? option[:file] then + puts "The file does not exist!.. #{option[:file]}" + exit(-1) + end + + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + # FTP INFO + client = BuildCommClient.create( result[0], result[1], nil, 0 ) + if client.nil? then + puts "Can't access server #{result[0]}:#{result[1]}" + exit(-1) + end + client.send "QUERY|FTP" + result0 = client.receive_data() + if result0.nil? then client.terminate + exit(-1) end + result0 = result0[0].split(",").map { |x| x.strip } + ip = result0[0] + username = result0[1] + passwd = result0[2] + client.terminate - # JOB INFO - client = BuildCommClient.create( option[:domain], option[:port]) - if not client.nil? then - client.send "QUERY,JOB" - result1 = client.receive_data() - if result0.nil? then - client.terminate - exit(-1) - end - puts "* JOB *" - for item in result1 - tok = item.split(",").map { |x| x.strip } - puts "#{tok[1]} #{tok[0]} #{tok[2]}" - end + client = BuildCommClient.create( result[0], result[1], nil, 0 ) + if client.nil? then + puts "Can't access server #{result[0]}:#{result[1]}" + exit(-1) + end + client.send("UPLOAD") + result = client.send_file(ip, username, passwd, option[:file]) + client.terminate + if not result then + puts "Uploading file failed!.. #{option[:file]}" + exit(-1) + else + puts "Uploading file succeeded!" + end + + when "download" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + # FTP INFO + client = BuildCommClient.create( result[0], result[1], nil, 0 ) + if client.nil? then + puts "Can't access server #{result[0]}:#{result[1]}" + exit(-1) end + client.send "QUERY|FTP" + result0 = client.receive_data() + if result0.nil? then + client.terminate + exit(-1) + end + result0 = result0[0].split(",").map { |x| x.strip } + ip = result0[0] + username = result0[1] + passwd = result0[2] + client.terminate + # download + client = BuildCommClient.create( result[0], result[1], nil, 0 ) + if client.nil? then + puts "Can't access server #{result[0]}:#{result[1]}" + exit(-1) + end + file_name = option[:file] + client.send("DOWNLOAD|#{file_name}") + result = client.receive_file(ip, username, passwd, "./#{file_name}") + client.terminate + if not result then + puts "Downloading file failed!.. #{option[:file]}" + exit(-1) + else + puts "Downloading file succeeded!" + end else raise RuntimeError, "input option incorrect : #{option[:cmd]}" end diff --git a/build-svr b/build-svr index 66aed20..fd881ff 100755 --- a/build-svr +++ b/build-svr @@ -1,4 +1,4 @@ -#!/usr/bin/ruby -d +#!/usr/bin/ruby -d =begin @@ -32,6 +32,7 @@ require 'fileutils' $LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" $LOAD_PATH.unshift File.dirname(__FILE__)+"/src/build_server" require "utils" +require "log.rb" require "BuildServerOptionParser" require "BuildServerController" @@ -39,49 +40,113 @@ require "BuildServerController" begin option = option_parse rescue => e - puts "Option parse error" puts e.message exit 0 end +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end -# if "--os" is not specified, use host os type +# if "--os" is not specified, set it as default if option[:os].nil? then - host_os = `uname -s`.strip - case host_os - when "Linux" - option[:os] = "linux" - when /MINGW32.*/ - option[:os] = "windows" - when "Darwin" - option[:os] = "darwin" - else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end - end -else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end + option[:os] = "default" end - - begin - case option[:cmd] - when "create" - BuildServerController.create_server( option[:name], Utils::WORKING_DIR, option[:url], option[:domain], option[:pid] ) + case option[:cmd] + when "create" + svr_result = Utils.parse_server_addr(option[:domain]) + if svr_result.nil? or svr_result.length != 2 then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + ftp_result = Utils.parse_ftpserver_url(option[:fdomain]) + if ftp_result.nil? or ftp_result.length != 4 then + puts "FTP server url is incorrect. (#{option[:fdomain]})" + puts "Tune as following format." + puts " ftp://:@
:" + exit 1 + end + pkgsvr_addr = svr_result[0] + pkgsvr_port = svr_result[1] + ftpsvr_addr = ftp_result[0] + ftpsvr_port = ftp_result[1] + ftpsvr_username = ftp_result[2] + ftpsvr_passwd = ftp_result[3] + BuildServerController.create_server( option[:name], Utils::WORKING_DIR, option[:url], pkgsvr_addr, pkgsvr_port, option[:pid], ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd ) when "remove" BuildServerController.remove_server( option[:name] ) when "start" - BuildServerController.start_server( option[:name], option[:port] ) + if( option[:child] ) then # Child Process + BuildServerController.start_server( option[:name], option[:port] ) + else # Parent Process + log = Log.new( "#{BuildServer::CONFIG_ROOT}/#{option[:name]}/main.log" ) + begin + while(true) + log.info "Build Server[#{option[:name]}] Start - PORT:[#{option[:port]}]" + # Start child process + cmd = Utils.execute_shell_generate("#{File.dirname(__FILE__)}/build-svr start -n #{option[:name]} -p #{option[:port]} --CHILD") + IO.popen(cmd) + pid = Process.wait + + # End chlid process + log.info "Child process terminated, pid = #{pid}, status = #{$?.exitstatus}" + if ($?.exitstatus == 0) then # SERVER STOP COMMAND + log.info "Down Build Server." + break + elsif ($?.exitstatus == 99) then # DIBS UPGRADE + cmd = "#{File.dirname(__FILE__)}/upgrade -l #{File.dirname(__FILE__)} -S -t BUILDSERVER -n #{option[:name]} -p #{option[:port]}" + cmd = Utils.execute_shell_generate(cmd) + puts cmd + Utils.spawn(cmd) + log.info cmd + log.info "Down Build Server for DIBS upgrade." + break + else + log.error "Down Build Server. Try reboot Build Server." + end + end + rescue => e + log.error( e.message, Log::LV_USER) + end + end when "stop" BuildServerController.stop_server( option[:name] ) - when "add" - BuildServerController.add_friend_server( option[:name], option[:domain], option[:port] ) + when "upgrade" + BuildServerController.upgrade_server( option[:name] ) + when "add-svr" + if not option[:domain].nil? then + svr_result = Utils.parse_server_addr(option[:domain]) + if svr_result.nil? or svr_result.length != 2 then + puts "Server address is incorrect. Tune as following format." + puts " :" + exit 1 + end + pkgsvr_addr = svr_result[0] + pkgsvr_port = svr_result[1] + BuildServerController.add_friend_server( option[:name], pkgsvr_addr, pkgsvr_port ) + elsif not option[:url].nil? then + BuildServerController.add_remote_package_server( option[:name], option[:url], option[:proxy] ) + end + when "add-prj" + if not option[:git].nil? then + BuildServerController.add_project( option[:name], option[:pid], + option[:git], option[:branch], option[:remote], option[:passwd], option[:os] ) + else + BuildServerController.add_binary_project( option[:name], option[:pid], + option[:package], option[:passwd], option[:os] ) + end + when "add-os" + BuildServerController.add_target_os( option[:name], option[:os] ) + when "fullbuild" + BuildServerController.build_all_projects( option[:name] ) + when "register" + BuildServerController.register_package( option[:name], option[:package] ) else raise RuntimeError, "input option incorrect : #{option[:cmd]}" end diff --git a/doc/DIBS_Advanced_Guide.pdf b/doc/DIBS_Advanced_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4e90588f3470e91c7cf971babc7d125d30c4ada4 GIT binary patch literal 290553 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πfGATeV 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
DfGl7Xh+~T#uNN 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

-HnLz3~2RJGkFm-#d!vr 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

Din5CAUUKuc!*U?4jm{oJTWJOX|qW0uS$q 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 V&%j^~kpY3DDXoD6Eyr7F{R7y6+erWb literal 0 HcmV?d00001 diff --git a/doc/Tizen_SDK_Package_Guide.pdf b/doc/Tizen_SDK_Package_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5fd43ac8f1d112cb52a82f7b7306f0de01467b9c GIT binary patch literal 22175 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 b&SFff;nuw#yN9}tZ;TB6i# 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 F`2}D0Qqcea literal 0 HcmV?d00001 diff --git a/package/build.linux b/package/build.linux index 7ff849a..05ef573 100755 --- a/package/build.linux +++ b/package/build.linux @@ -15,20 +15,17 @@ build() # install install() { - BIN_DIR=$SRCDIR/package/dibs.package.${BUILD_TARGET_OS}/data/dev_tools - DOC_DIR=$SRCDIR/package/dibs.package.${BUILD_TARGET_OS}/data/dev_tools/doc + BIN_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/tools/dibs/ + DOC_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/documents mkdir -p $BIN_DIR mkdir -p $DOC_DIR cp -f $SRCDIR/pkg-* $BIN_DIR/ cp -f $SRCDIR/build-* $BIN_DIR/ cp -rf $SRCDIR/src $BIN_DIR/ - cp -rf $SRCDIR/src $BIN_DIR/ + cp -f $SRCDIR/upgrade $BIN_DIR/ cp -f $SRCDIR/AUTHORS $DOC_DIR/ cp -f $SRCDIR/LICENSE $DOC_DIR/ cp -f $SRCDIR/NOTICE $DOC_DIR/ + cp -f $SRCDIR/doc/* $DOC_DIR/ + echo $VERSION > $BIN_DIR/VERSION } - - -$1 -echo "$1 success" - diff --git a/package/build.macos b/package/build.macos new file mode 100644 index 0000000..05ef573 --- /dev/null +++ b/package/build.macos @@ -0,0 +1,31 @@ +#!/bin/sh -xe +# clean +clean() +{ + rm -rf $SRCDIR/*.zip + rm -rf $SRCDIR/*.tar.gz +} + +# build +build() +{ + echo "build" +} + +# install +install() +{ + BIN_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/tools/dibs/ + DOC_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/documents + mkdir -p $BIN_DIR + mkdir -p $DOC_DIR + cp -f $SRCDIR/pkg-* $BIN_DIR/ + cp -f $SRCDIR/build-* $BIN_DIR/ + cp -rf $SRCDIR/src $BIN_DIR/ + cp -f $SRCDIR/upgrade $BIN_DIR/ + cp -f $SRCDIR/AUTHORS $DOC_DIR/ + cp -f $SRCDIR/LICENSE $DOC_DIR/ + cp -f $SRCDIR/NOTICE $DOC_DIR/ + cp -f $SRCDIR/doc/* $DOC_DIR/ + echo $VERSION > $BIN_DIR/VERSION +} diff --git a/package/build.windows b/package/build.windows new file mode 100644 index 0000000..05ef573 --- /dev/null +++ b/package/build.windows @@ -0,0 +1,31 @@ +#!/bin/sh -xe +# clean +clean() +{ + rm -rf $SRCDIR/*.zip + rm -rf $SRCDIR/*.tar.gz +} + +# build +build() +{ + echo "build" +} + +# install +install() +{ + BIN_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/tools/dibs/ + DOC_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/documents + mkdir -p $BIN_DIR + mkdir -p $DOC_DIR + cp -f $SRCDIR/pkg-* $BIN_DIR/ + cp -f $SRCDIR/build-* $BIN_DIR/ + cp -rf $SRCDIR/src $BIN_DIR/ + cp -f $SRCDIR/upgrade $BIN_DIR/ + cp -f $SRCDIR/AUTHORS $DOC_DIR/ + cp -f $SRCDIR/LICENSE $DOC_DIR/ + cp -f $SRCDIR/NOTICE $DOC_DIR/ + cp -f $SRCDIR/doc/* $DOC_DIR/ + echo $VERSION > $BIN_DIR/VERSION +} diff --git a/package/pkginfo.manifest b/package/pkginfo.manifest index 8f8eda2..cf38c95 100644 --- a/package/pkginfo.manifest +++ b/package/pkginfo.manifest @@ -1,15 +1,8 @@ -Package : dibs -Version : 0.20.9 -Maintainer : taejun ha, jiil hyoun , , donghee yang< donghee.yang@samsung.com > -Description : Distribute Inteligent Build System -OS : linux -Build-host-os : linux Source : dibs +Version :1.0.6 +Maintainer : taejun ha, jiil hyoun , donghyuk yang , donghee yang , sungmin kim , jiil hyoun , , donghee yang< donghee.yang@samsung.com > +OS : ubuntu-32, ubuntu-64, windows-32, windows-64, macos-64 +Build-host-os : ubuntu-32 Description : Distribute Inteligent Build System -OS : windows -Build-host-os : linux -Source : dibs diff --git a/package/pkginfo.manifest.local b/package/pkginfo.manifest.local new file mode 100644 index 0000000..ce655de --- /dev/null +++ b/package/pkginfo.manifest.local @@ -0,0 +1,6 @@ +Include: pkginfo.manifest + +Package : dibs +OS : ubuntu-32, windows-32, macos-64, ubuntu-64, windows-64 +Build-host-os : windows-32, macos-64, ubuntu-64, windows-64 +Description : Distribute Inteligent Build System diff --git a/pkg-build b/pkg-build index 22b58f3..39e7882 100755 --- a/pkg-build +++ b/pkg-build @@ -36,18 +36,24 @@ require "packageServer" require "Builder" require "optionparser" -option = parse +begin + option = parse +rescue => e + puts e.message + exit 0 +end #generate server when local package server is not set +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end + # if "--os" is not specified, use host os type if option[:os].nil? then option[:os] = Utils::HOST_OS -else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end end path = Dir.pwd @@ -62,25 +68,27 @@ if not option[:url].nil? then builder = Builder.get("default") if builder.pkgserver_url != option[:url] then puts "Package server URL has been changed! Creating new builder..." - builder = Builder.create("default", option[:url], nil) + builder = Builder.create("default", option[:url], nil, nil, nil) end rescue puts "Default builder does not exist! Creating new builder..." - builder = Builder.create("default", option[:url], nil) + builder = Builder.create("default", option[:url], nil, nil, nil) end else # if url is not specified begin builder = Builder.get("default") rescue puts "Default builder does not exist! Creating new builder..." - builder = Builder.create("default", "http://172.21.111.132/pkgserver/unstable",nil) + builder = Builder.create("default", "http://172.21.111.132/pkgserver/unstable",nil, nil, nil) end end #build project -if not builder.build( Utils::WORKING_DIR, option[:os], option[:clean], option[:rev], [], []) then +if not builder.build( Utils::WORKING_DIR, option[:os], option[:clean], [], true) then puts "Build Failed!" + exit 1 else puts "Build Succeeded!" + exit 0 end diff --git a/pkg-clean b/pkg-clean index de48dce..9315d88 100755 --- a/pkg-clean +++ b/pkg-clean @@ -46,6 +46,12 @@ option = parse #generate server when local package server is not set +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end + begin builder = Builder.get("default") rescue diff --git a/pkg-cli b/pkg-cli index 3a04581..df9a164 100755 --- a/pkg-cli +++ b/pkg-cli @@ -42,93 +42,73 @@ require "packageServer" #set global variable @WORKING_DIR = nil -$log = Logger.new('.log', 'monthly') - #option parsing begin option = option_parse rescue => e # if option parse error print help message - $log.error "option parsing error" - system "#{__FILE__} help" + puts e.message exit 0 end +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end #if "--os" is not specfied, use host os type if option[:os].nil? then - system_type = `uname -s` - case system_type.strip - when "Linux" then - option[:os] = "linux" - when /MINGW32.*/ then - option[:os] = "windows" - when "Darwin" then - option[:os] = "darwin" - else - raise RuntimeError, "Unknown OS type : #{system_type}" - end + option[:os] = Utils::HOST_OS end case option[:cmd] when "update" then client = Client.new( option[:url], nil, nil ) - client.update() + #client.update() when "clean" then client = Client.new( nil, option[:loc], nil ) client.clean(option[:f]) when "download" then client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end + #if not option[:url].nil? then + # client.update() + #end file_loc = client.download( option[:pkg], option[:os], option[:t] ) -when "upload" then - client = Client.new( nil, nil, nil ) - result = client.upload( option[:alias], option[:id], option[:binpkg], option[:srcpkg], false ) - if not result.nil? then - puts result - end -when "source" then - client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end - client.download_source( option[:pkg], option[:os] ) when "install" then client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end - client.install( option[:pkg], option[:os], option[:t], option[:f] ) + #if not option[:url].nil? then + # client.update() + #end + client.install( option[:pkg], option[:os], option[:t], option[:f] ) when "install-file" then - client = Client.new( nil, option[:loc], nil ) - client.install_local_pkg( option[:pkg], option[:f] ) + client = Client.new( option[:url], option[:loc], nil ) + client.install_local_pkg( option[:pkg], option[:t], option[:f] ) when "uninstall" then client = Client.new( nil, option[:loc], nil ) client.uninstall( option[:pkg], option[:t] ) when "upgrade" then client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end + #if not option[:url].nil? then + # client.update() + #end client.upgrade( option[:os], option[:t] ) when "check-upgrade" then client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end + #if not option[:url].nil? then + # client.update() + #end client.check_upgrade( option[:os] ) when "show-rpkg" then client = Client.new( option[:url], nil, nil ) - if not option[:url].nil? then - client.update() - end + #if not option[:url].nil? then + # client.update() + #end puts client.show_pkg_info( option[:pkg], option[:os] ) when "list-rpkg" then client = Client.new( option[:url], nil, nil ) - if not option[:url].nil? then - client.update() - end + #if not option[:url].nil? then + # client.update() + #end result = client.show_pkg_list( option[:os] ) if not result.nil? and not result.empty? then result.each do |i| @@ -175,6 +155,6 @@ when "install-dep" then ret[-3..-1] = "" puts ret else - raise RuntimeError, "input option incorrect : #{option[:cmd]}" + raise RuntimeError, "Input is incorrect : #{option[:cmd]}" end diff --git a/pkg-svr b/pkg-svr index 7a80d61..3e3d597 100755 --- a/pkg-svr +++ b/pkg-svr @@ -39,10 +39,7 @@ require "serverOptParser" begin option = option_parse rescue => e - puts "\n=============== Error occured ==============================" puts e.message - puts e.backtrace.inspect - puts "=============================================================\n" exit 0 end @@ -66,15 +63,15 @@ begin when "create" server.create( option[:id], option[:dist], option[:url], option[:loc] ) when "register" - server.register( option[:spkgs], option[:bpkgs], option[:dist], option[:gensnap], option[:test] ) + server.register( option[:pkgs], option[:dist], option[:gensnap], option[:test], false ) when "gen-snapshot" - server.generate_snapshot( option[:snap], option[:dist], option[:bsnap], option[:bpkgs] ) + server.generate_snapshot( option[:snaps][0], option[:dist], option[:bsnap] ) when "sync" server.sync( option[:dist], option[:force] ) when "add-dist" server.add_distribution( option[:dist], option[:url], option[:clone] ) - when "spkg-path" - server.find_source_package_path( option[:dist], option[:spkgs] ) + when "add-os" + server.add_os( option[:dist], option[:os] ) when "remove" if not option[:force] then puts "Do you want to really? then input \"YES\"" @@ -87,17 +84,35 @@ begin end end - server.remove_server( option[:id] ) + server.remove_server() + when "remove-dist" + if not option[:force] then + puts "Do you want to really? then input \"YES\"" + input = $stdin.gets.strip + if input.eql? "YES" then + puts "Remove server!" + else + puts "Remove is canceled by user input" + exit(0) + end + end + + server.remove_dist( option[:dist] ) when "remove-pkg" - server.remove_pkg( option[:id], option[:dist], option[:bpkgs], option[:os] ) + server.remove_pkg( option[:dist], option[:pkgs], option[:os] ) + when "remove-snapshot" + server.remove_snapshot( option[:dist], option[:snaps] ) + when "clean" + server.clean( option[:dist], option[:snaps] ) + when "start" + server.start( option[:port], option[:passwd] ) + when "stop" + server.stop( option[:port], option[:passwd] ) else raise RuntimeError, "input option incorrect : #{option[:cmd]}" end rescue => e - puts "\n=============== Error occured ==============================" puts e.message - puts e.backtrace.inspect - puts "=============================================================\n" end diff --git a/src/build_server/BinaryUploadProject.rb b/src/build_server/BinaryUploadProject.rb new file mode 100644 index 0000000..28f81ea --- /dev/null +++ b/src/build_server/BinaryUploadProject.rb @@ -0,0 +1,91 @@ +=begin + + BinaryUploadProject.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +$LOAD_PATH.unshift File.dirname(__FILE__) +require "CommonProject.rb" +require "RegisterPackageJob.rb" +require "Version.rb" +require "PackageManifest.rb" + + +class BinaryUploadProject < CommonProject + attr_accessor :pkg_name + + # initialize + def initialize( name, pkg_name, server, os_list ) + super(name, "BINARY", server, os_list) + @pkg_name = pkg_name + end + + + # create new job + def create_new_job( filename, dock = "0" ) + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + pkg_name = new_name.split(",")[0] + os = new_name.split(",")[2] + + # check file name + if @pkg_name != pkg_name then return nil end + + # check os name + if not @server.supported_os_list.include? os then return nil end + + # check package info + file_path = "#{@server.transport_path}/#{dock}/#{filename}" + if not File.exist? file_path then return nil end + + pkginfo_dir = "#{@server.path}/projects/#{@name}/pkginfos" + if not File.exist? pkginfo_dir then FileUtils.mkdir_p pkginfo_dir end + if not Utils.extract_a_file(file_path, "pkginfo.manifest", pkginfo_dir) then + return nil + end + begin + pkginfo =PackageManifest.new("#{pkginfo_dir}/pkginfo.manifest") + rescue => e + puts e.message + return nil + end + pkgs = pkginfo.get_target_packages(os) + if pkgs.count != 1 then return nil end + if pkgs[0].package_name != @pkg_name then return nil end + + new_job = RegisterPackageJob.new( file_path, self, @server ) + + return new_job + end + + + def include_package?(name, version=nil, os=nil) + if name == @pkg_name then + return true + else + return false + end + end +end diff --git a/src/build_server/BuildClientOptionParser.rb b/src/build_server/BuildClientOptionParser.rb index 1adf0b0..8cc9e60 100644 --- a/src/build_server/BuildClientOptionParser.rb +++ b/src/build_server/BuildClientOptionParser.rb @@ -26,51 +26,151 @@ Contributors: - S-Core Co., Ltd =end +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" require 'optparse' +require 'utils' -def option_parse +def option_error_check( options ) + case options[:cmd] + + when "build" then + if options[:project].nil? or options[:project].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli build -N -d [-o ] [-w ] [--async]" + end + + when "resolve" then + if options[:project].nil? or options[:project].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli resolve -N -d [-o ] [-w ] [--async]" + end + + when "query" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli query -d " + end + + when "query-system" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli query-system -d " + end + + when "query-project" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli query-project -d " + end + + when "query-job" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli query-job -d " + end + + when "cancel" then + if options[:job].nil? or options[:job].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: build-cli cancel -j -d [-w ]" + end + when "register" then + if options[:package].nil? or options[:package].empty? or + options[:domain].nil? or options[:domain].empty? or + options[:fdomain].nil? or options[:fdomain].empty? then + raise ArgumentError, "Usage: build-cli register -P -d -t [-w ]" + end + + else + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" + end + + if ARGV.length > 1 then + raise ArgumentError, "Unknown argument value : #{ARGV[1]}" + end +end + +def option_parse options = {} - banner = "Usage: build-cli {build|resolve|query} ..." + "\n" \ - + "\t" + "build-cli build -g -c [-d ] [-p ] [-o ] [-a ] " + "\n" \ - + "\t" + "build-cli resolve -g -c [-d ] [-p ] [-o ] [-a ] " + "\n" \ - + "\t" + "build-cli query [-d ] [-p ]" + "\n" + banner = "Requiest service to build-server command-line tool." + "\n" \ + + "\n" + "Usage: build-cli [OPTS] or build-cli (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "build Build and create package." + "\n" \ + + "\t" + "resolve Request change to resolve-status for build-conflict." + "\n" \ + + "\t" + "query Query information about build-server." + "\n" \ + + "\t" + "query-system Query system information about build-server." + "\n" \ + + "\t" + "query-project Query project information about build-server." + "\n" \ + + "\t" + "query-job Query job information about build-server." + "\n" \ + + "\t" + "cancel Cancel a building project." + "\n" \ + + "\t" + "register Register the package to the build-server." + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + "build-cli build -N -d [-o ] [-w ] [--async]" + "\n" \ + + "\t" + "build-cli resolve -N -d [-o ] [-w ] [--async]" + "\n" \ + + "\t" + "build-cli query -d " + "\n" \ + + "\t" + "build-cli query-system -d " + "\n" \ + + "\t" + "build-cli query-project -d " + "\n" \ + + "\t" + "build-cli query-job -d " + "\n" \ + + "\t" + "build-cli cancel -j -d [-w ] " + "\n" \ + + "\t" + "build-cli register -P -d -t [-w ] " + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| - optparse = OptionParser.new do|opts| # Set a banner, displayed at the top # of the help screen. opts.banner = banner - opts.on( '-g', '--git ', 'git repository' ) do|git| - options[:git] = git - end - - opts.on( '-c', '--commit ', 'git commit id/tag' ) do|git| - options[:commit] = git + opts.on( '-N', '--project ', 'project name' ) do|project| + if not Utils.multi_argument_test( project, "," ) then + raise ArgumentError, "Project variable parsing error : #{project}" + end + options[:project] = project end options[:domain] = nil - opts.on( '-d', '--domain ', 'remote build server ip address. default 127.0.0.1' ) do|domain| + opts.on( '-d', '--address ', 'build server address: 127.0.0.1:2224' ) do|domain| options[:domain] = domain end - options[:port] = nil - opts.on( '-p', '--port ', 'remote build server port. default 2222' ) do|port| - options[:port] = port - end - options[:os] = nil - opts.on( '-o', '--os ', 'target operating system linux/windows/darwin' ) do|os| + opts.on( '-o', '--os ', 'target operating system: ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64' ) do |os| + if not Utils.multi_argument_test( os, "," ) then + raise ArgumentError, "OS variable parsing error : #{os}" + end options[:os] = os end options[:async] = "NO" - opts.on( '-a', '--async', 'asynchronous job' ) do + opts.on( '--async', 'asynchronous job' ) do options[:async] = "YES" + end + + options[:noreverse] = "NO" + opts.on( '--noreverse', 'do not check reverse build' ) do + options[:noreverse] = "YES" end - opts.on( '-h', '--help', 'display this information' ) do - puts opts + opts.on( '-j', '--job ', 'job number' ) do|job| + options[:job] = job + end + + options[:passwd] = "" + opts.on( '-w', '--passwd ', 'password for managing project' ) do|passwd| + options[:passwd] = passwd + end + + opts.on( '-P', '--pkg ', 'package file path' ) do|package| + options[:package] = package.strip + end + + opts.on( '-t', '--ftp ', 'ftp server url: ftp://dibsftp:dibsftp@127.0.0.1' ) do|domain| + options[:fdomain] = domain + end + + opts.on( '-h', '--help', 'display help' ) do + opts.help.split("\n").each {|op| puts op if not op.include? "--noreverse"} + exit + end + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() exit end @@ -78,19 +178,26 @@ def option_parse cmd = ARGV[0] - if cmd.eql? "build" or cmd.eql? "resolve" or cmd.eql? "query" or - cmd =~ /(help)|(-h)|(--help)/ then + if cmd.eql? "build" or cmd.eql? "resolve" or + cmd.eql? "query" or cmd.eql? "query-system" or + cmd.eql? "query-project" or cmd.eql? "query-job" or + cmd.eql? "cancel" or + cmd.eql? "register" or + cmd =~ /(-v)|(--version)/ or + cmd =~ /(help)|(-h)|(--help)/ then - if cmd.eql? "help" then + if cmd.eql? "help" then ARGV[0] = "-h" end options[:cmd] = ARGV[0] else - raise ArgumentError, banner + raise ArgumentError, "Usage: build-cli [OPTS] or build-cli -h" end optparse.parse! + + option_error_check options return options end diff --git a/src/build_server/BuildComm.rb b/src/build_server/BuildComm.rb index 94f8bb1..647d755 100644 --- a/src/build_server/BuildComm.rb +++ b/src/build_server/BuildComm.rb @@ -30,16 +30,45 @@ Contributors: $LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" require "log" +require 'timeout' +require "fileTransfer" +require "net/ftp" +require 'thread' + +ATTEMPTS = ["first", "second", "third"] class BuildCommServer - VERSION = "1.2.0" + VERSION = "1.5.0" + + private_class_method :new - def initialize(port, log) + def initialize(port, log, ftp_url, cache_dir) @port = port - @tcp_server = TCPServer.open( port ) @log = log + @ftp_url = ftp_url + @cache_dir = cache_dir + @tcp_server = TCPServer.open( port ) + @download_cache_mutex = Mutex.new end + def self.create(port, log, ftp_url=nil, cache_dir=nil) + # checking port is available + if port_open? port then + raise "Port \"#{@port}\" is already in use." + end + + if log.nil? then + log = Log.new(nil) + end + + # create cache dir if not nil + if not cache_dir.nil? and not File.exist? cache_dir then + FileUtils.mkdir_p cache_dir + end + + return new(port, log, ftp_url, cache_dir) + end + # wait for connection and handle request def wait_for_connection(quit_loop) @@ -47,8 +76,9 @@ class BuildCommServer req = @tcp_server.accept begin - yield req + yield req if block_given? rescue + @log.error $! @log.error "Caught a connection exception" req.close end @@ -89,30 +119,275 @@ class BuildCommServer end + def send_file(req, src_file) + # 1. send "READY" + # 2. If "FTP,ip,username,passwd" is received, + # Upload the src file using server's ftp_url. + # if then ftp_url is nil, use the url on "FTP" message instead + # After uploading, send "UPLOADED,ftp_filepath" + # 3. If "SUCC" is received, remove the file on FTP server + + begin + if not File.exist? src_file then + @log.error "\"#{src_file}\" file does not exist" + req.puts "ERROR" + return false + end + + req.puts "READY" + @log.info "Ready to upload file" + while l = req.gets() + tok = l.split(",").map { |x| x.strip } + cmd = tok[0].strip + if cmd == "FTP" then + if tok.count < 5 then + @log.error "Server received wrong REQ : #{l.strip}" + req.puts "ERROR" + return false + end + + # get ftp connection info + if @ftp_url.nil? then + ip = tok[1].strip + port = tok[2].strip + username = tok[3].strip + passwd = tok[4].strip + @log.info "Server received ftp server infomation from client : [#{ip}, #{port}]" + else + url_contents = Utils.parse_ftpserver_url(@ftp_url) + ip = url_contents[0] + port = url_contents[1] + username = url_contents[2] + passwd = url_contents[3] + end + + # upload to ftp server + ftp_filepath = nil + for attempt in ATTEMPTS + ftp_filepath = FileTransfer.putfile(ip, port, username, passwd, src_file, @log) + if !ftp_filepath.nil? then break; + else @log.info "Server is the #{attempt} upload attempt fails" end + end + if ftp_filepath.nil? then + req.puts "ERROR" + return false + else + @log.info "Server is the #{attempt} successful attempt to upload file: [#{File.basename(src_file)}]" + end + req.puts "UPLOADED,#{ftp_filepath}" + elsif cmd == "SUCC" then + @log.info "Client downloaded file successfully" + FileTransfer.cleandir(ip, port, username, passwd, ftp_filepath, @log) + @log.info "Cleaned temporary dir on FTP server: #{ftp_filepath}" + break + elsif cmd == "ERROR" then + @log.error "Client failed to download file" + return false + end + end + rescue => e + puts "[BuildCommServer] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return true + end + + + # NOTE. dst_file can be directory + def receive_file(req, dst_file) + # 1. send "READY" + # 2. If "UPLOADED,ip,port,file_path,username,passwd" is received, + # Download the file using my ftp_url. + # If ftp_url is nil, use the url on "UPLOADED" messge instead + # After downloading it, send "SUCC" + + begin + req.puts "READY" + while l = req.gets() + tok = l.split(",").map { |x| x.strip } + cmd = tok[0].strip + if cmd == "CHECK_CACHE" then + file_name = tok[1] + file_size = tok[2].to_i + checksum = tok[3] + + # check download cache + if File.exist? dst_file and File.directory? dst_file then + target_file = File.join(dst_file,file_name) + else + target_file = dst_file + end + if not @cache_dir.nil? and + check_download_cache( target_file, file_size, checksum ) then + + @log.info "Download cache hit! Copied from cache.: #{file_name}" + req.puts "CACHED" + break + else + @log.info "Cached file not found!#{file_name}" + req.puts "NOT_CACHED" + end + elsif cmd == "UPLOADED" then + @log.info "Client uploaded file to ftp server successful" + if tok.count < 6 then + @log.error "Server received wrong REQ : #{l.strip}" + req.puts "ERROR" + return false + end + filepath = tok[3].strip + + # get ftp connection info + if @ftp_url.nil? then + ip = tok[1].strip + port = tok[2].strip + username = tok[4].strip + passwd = tok[5].strip + @log.info "Client sent ftp server infomations [#{ip}, #{port}]" + else + url_contents = Utils.parse_ftpserver_url(@ftp_url) + ip = url_contents[0] + port = url_contents[1] + username = url_contents[2] + passwd = url_contents[3] + end + + # download from ftp server + dst_filepath = nil + for attempt in ATTEMPTS + dst_filepath = FileTransfer.getfile(ip, port, username, passwd, filepath, dst_file, @log) + if not dst_filepath.nil? then break + else + @log.warn "Server is the #{attempt} download attempt fails" + end + end + if dst_filepath.nil? then + req.puts "ERROR" + return false + else @log.info " Server is the #{attempt} successful attempt to download" end + + # add to cache + if not @cache_dir.nil? then + if File.exist? dst_file and File.directory? dst_file then + target_file = File.join(dst_file,File.basename(dst_filepath)) + else + target_file = dst_file + end + add_download_cache(target_file) + end + + req.puts "SUCC" + break + elsif cmd == "ERROR" then + @log.error "Client failed to upload the file" + return false + else + @log.warn "Unhandled message: #{l}" + end + end + rescue => e + puts "[BuildCommServer] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return true + end + + def self.disconnect( req ) begin req.close rescue end end + + def self.port_open?( port ) + Timeout::timeout(1) do + begin + TCPSocket.new("127.0.0.1",port).close + true + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH + false + end + end + rescue Timeout::Error + false + end + + + private + def check_download_cache(dst_file, file_size, checksum ) + file_name = File.basename(dst_file) + cache_file = "#{@cache_dir}/#{file_name}" + + @download_cache_mutex.synchronize { + found = false + # check file exist + if File.exist? cache_file and + File.size(cache_file) == file_size and + Utils.checksum(cache_file) == checksum then + + # if hit , touch and copy + FileUtils.touch cache_file + FileUtils.copy_file(cache_file, dst_file) + + found = true + end + + # refresh cache dir + curr_time = Time.now + Dir.entries(@cache_dir).each { |fname| + if fname == "." or fname == ".." then next end + file_path = "#{@cache_dir}/#{fname}" + if File.mtime(file_path) + 3600 < curr_time then + FileUtils.rm_rf file_path + end + } + + return found + } + end + + + private + def add_download_cache(dst_file) + file_name = File.basename(dst_file) + cache_file = "#{@cache_dir}/#{file_name}" + @download_cache_mutex.synchronize { + # copy & touch + FileUtils.copy_file(dst_file, cache_file) + FileUtils.touch cache_file + } + end end class BuildCommClient - VERSION = "1.2.0" + VERSION = "1.5.0" private_class_method :new - def initialize(socket) + def initialize(socket, log) + @log = log @socket = socket end # create - def self.create(ip, port) + # if sec 0 or nil then not set timeout. it's timeout spec + def self.create(ip, port, log = nil, sec = 5) # open socket + socket = nil begin - socket = TCPSocket.open( ip, port ) + timeout(sec) do + socket = TCPSocket.open( ip, port ) + end + rescue Timeout::Error + return nil rescue # unknown exception return nil @@ -120,10 +395,14 @@ class BuildCommClient # refused if socket.nil? then - return nil - end + return nil + end + + if log.nil? then + log = Log.new(nil) + end - return new(socket) + return new(socket, log) end @@ -140,7 +419,7 @@ class BuildCommClient begin l = @socket.gets() - if @socket.nil? then + if l.nil? then puts "Connection refused" return false end @@ -173,9 +452,12 @@ class BuildCommClient begin # get first line - l = @socket.gets() + l = nil + timeout(5) do + l = @socket.gets() + end - if @socket.nil? then + if l.nil? then return false end @@ -190,9 +472,13 @@ class BuildCommClient if line.strip == "=CHK" then next end # print - yield line.strip + yield line.strip if block_given? end - rescue + rescue Timeout::Error + puts "WARN: Connection timed out" + return false + rescue => e + puts e.message return false end @@ -207,7 +493,7 @@ class BuildCommClient begin l = @socket.gets() - if @socket.nil? then + if l.nil? then puts "Connection refused" return nil end @@ -235,6 +521,133 @@ class BuildCommClient end + def send_file(ip, port, username, passwd, src_file) + begin + l = @socket.gets() + if l.nil? then + @log.error "[BuildCommClient] Connection refused" + return false + end + + # check protocol + if not protocol_matched? l.strip then + @log.error "[BuildCommClient] Comm. Protocol version is mismatched! #{VERSION}" + return false + end + + # 1. If "READY" is received, upload src file to FTP server + # After uploading it, send "UPLOADED,ip,file_path,username,passwd" + # 2. If "SUCC" is received, remove the file on FTP server + while line = @socket.gets() + if line.strip == "READY" then + @log.info "Server is ready to receive file" + file_name = File.basename(src_file) + file_size = File.size(src_file) + checksum = Utils.checksum(src_file) + send "CHECK_CACHE,#{file_name},#{file_size},#{checksum}" + elsif line.strip == "CACHED" then + @log.info "Server already has cached file" + elsif line.strip == "NOT_CACHED" then + @log.info "Server doest not have cached file" + ftp_filepath = nil + for attempt in ATTEMPTS + ftp_filepath = FileTransfer.putfile(ip, port, username, passwd, src_file, @log) + if !ftp_filepath.nil? then break; + else @log.info "Client is the #{attempt} upload attempt fails" end + end + if ftp_filepath.nil? then + send "ERROR" + return false + else @log.info "Client is the #{attempt} successful attempt to upload file" end + send "UPLOADED,#{ip},#{port},#{ftp_filepath},#{username},#{passwd}" + elsif line.strip == "SUCC" then + @log.info "Server downloaded file sucessfully" + FileTransfer.cleandir(ip, port, username, passwd, ftp_filepath, @log) + @log.info "Client cleaned temporary dir on ftp server: #{ftp_filepath}" + elsif line.strip == "ERROR" then + @log.error "Server failed to download the file. Please check server log" + return false + elsif line.strip == "=END" then + break + end + end + rescue => e + puts "[BuildCommClient] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return true + end + + + # return file + def receive_file(ip, port, username, passwd, dst_file) + begin + l = @socket.gets() + + if l.nil? then + @log.error "[BuildCommClient] Connection refused" + return false + end + + # check protocol + if not protocol_matched? l.strip then + @log.error "[BuildCommClient] Comm. Protocol version is mismatched! #{VERSION}" + return false + end + + # 1. If "READY" is received, send "FTP,ip,port,username,passwd" + # 2. if "UPLOADED,ftp_file_path" is received, + # Download the file + # Send "SUCC" + # 3. If "SUCC" is received, remove the file on FTP server + while line = @socket.gets() + cmd = line.split(",")[0].strip + #@log.info "[BuildCommClient] Received \"#{cmd}\" message from BuildCommServer" + if cmd == "READY" then + send "FTP,#{ip},#{port},#{username},#{passwd}" + @log.info "Client sent ftp server infomation to server : [#{ip}, #{port}]" + elsif cmd == "UPLOADED" then + tok = line.split(",") + if tok.length < 2 then + @log.error "Client received wrong REQ : #{line.strip}" + return false + end + ftp_filepath = tok[1].strip + @log.info "Server uploaded file sucessfully" + dst_filepath = nil + for attempt in ATTEMPTS + dst_filepath = FileTransfer.getfile(ip, port, username, passwd, ftp_filepath, dst_file, @log) + if not dst_filepath.nil? then break + else + @log.warn "Client is the #{attempt} download attempt fails" + end + end + if dst_filepath.nil? then + send "ERROR" + return false + else @log.info "Client is the #{attempt} successful attempt to download" end + send "SUCC" + elsif cmd == "ERROR" then + @log.error "Server failed to upload file. Check server log" + return false + elsif cmd == "=END" then + break + end + end + rescue => e + puts "[BuildCommServer] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return true + end + + def terminate @socket.close end diff --git a/src/build_server/BuildJob.rb b/src/build_server/BuildJob.rb index f29bdaf..223d752 100644 --- a/src/build_server/BuildJob.rb +++ b/src/build_server/BuildJob.rb @@ -1,5 +1,5 @@ =begin - + BuildJob.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -35,64 +35,318 @@ require "client.rb" require "PackageManifest.rb" require "Version.rb" require "Builder.rb" +require "RemoteBuilder.rb" require "BuildServer.rb" require "JobLog.rb" require "mail.rb" +require "utils.rb" +require "ReverseBuildChecker.rb" class BuildJob - attr_accessor :blocked_by + attr_accessor :id, :server, :pre_jobs, :os, :type + attr_accessor :status, :pkginfo, :log, :source_path + attr_accessor :pkgsvr_client, :thread + attr_accessor :rev_fail_projects, :rev_success_jobs + attr_accessor :pending_ancestor, :cancel_state + attr_accessor :no_reverse # initialize - def initialize () - @blocked_by = [] + def initialize (id, project, os, server) + @id = id + @project = project + @os = os + @server = server + @type = "BUILD" + + @status = "JUST_CREATED" + @cancel_state = "NONE" + @resolve = false + @host_os = Utils::HOST_OS + @pkgserver_url = @server.pkgserver_url + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + @pre_jobs = [] #pre-requisite jobs + + # this item will be initialized on pre-verify + @pkginfo = nil + @pkgsvr_client = nil + @thread = nil + @log = nil + @parent = nil # for job hierachy + + #for cancel operation + @pending_ancestor = nil # for cancel pending job + @remote_id = nil # for cancel remote_working job + @build_dep_prjs = nil # for cacnel pending job + + # for resolving build-break + @rev_fail_projects = [] # list of [project,os] + @rev_success_jobs = [] # list of job + + # remote build + @remote_server = nil + + # job type + @is_rev_build_check_job = false + @is_remote_job = false + + # for internal(transferred) job + @is_internal_job = false + @dock_num = "0" + + @external_pkgs = [] + @force_rebuild = false + + @no_reverse = false + end + + + def get_project() + return @project + end + + + # set parent + def set_parent_job( parent ) + # if parent exists, share build-root + @parent = parent + end + + # get parent + def get_parent_job() + return @parent + end + + + def is_sub_job? + return (not @parent.nil?) + end + + + def get_sub_jobs + return [] + end + + + def get_buildroot() + return @buildroot_dir + end + + + # set reverse build check job + def set_rev_build_check_job( parent ) + @is_rev_build_check_job = true + + # if parent exists, share build-root + if not parent.nil? then + set_parent_job( parent ) + end + end + + + def is_rev_build_check_job() + return @is_rev_build_check_job + end + + + def set_remote_job(server) + @is_remote_job = true + @remote_server=server + end + + def set_no_reverse() + @no_reverse = true + end + + + def set_internal_job( dock_num ) + @is_internal_job = true + @dock_num = dock_num + end + + + # set option for waiting for resolve + def set_resolve_flag() + @resolve = true + end + + + # set force rebuild + # This make project to build + # even though there is a package of same version on pkg-server + def set_force_rebuild(value) + @force_rebuild = value + end + + + # set logger + def set_logger( logger ) + @log = logger + end + + + # add external packages to overwrite before build + def add_external_package( file_name ) + @external_pkgs.push "#{@job_root}/external_pkgs/#{file_name}" end + # execute - def execute + def execute(sync=false) @log.info( "Invoking a thread for building Job #{@id}", Log::LV_USER) if @status == "ERROR" then return end @thread = Thread.new { - # main - thread_main() - - # close - terminate() - } + begin + thread_main() + if not is_sub_job? then terminate() end + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + } + + if sync then + @thread.join + end + + return true end - # remote - def execute_remote(server) - @log.info( "Invoking a thread for remote-building Job #{@id}", Log::LV_USER) - if @status == "ERROR" then return end - @thread = Thread.new { - # main - remote_thread_main( server ) - - # close - terminate() - } + #terminate + def terminate() + #do noting + end + + + #cancel + def cancel() + # cancel all its reverse job + @server.jobmgr.reverse_build_jobs.each do |job| + if job.get_parent_job() == self and job.cancel_state == "NONE" then + job.cancel_state = "INIT" + end + end + + # cancel log print + if not @log.nil? then + @log.info( "JOB is canceled by cancel operation !!", Log::LV_USER) + end + + case @status + when "REMOTE_WORKING" then + client = BuildCommClient.create( @remote_server.ip, @remote_server.port, @log ) + if not client.nil? then + client.send "CANCEL|#{@remote_id}|#{self.get_project.passwd}" + result1 = client.receive_data() + if result1.nil? then + @log.info( "cancel operation failed [connection error] !!", Log::LV_USER) + else + @log.info(result1, Log::LV_USER) + end + client.terminate + end + when "PENDING" then + if @pending_ancestor.nil? then + #resolve pending job + pending_descendants = @server.jobmgr.jobs.select do |j| + (not j.pending_ancestor.nil?) and "#{j.pending_ancestor.id}" == "#{@id}" + end + pending_descendants.each do |pd| + pd.cancel_state = "INIT" + end + else + # remove myself from success job if exist + # and add myself into rev_fail_project list if not exist + @pending_ancestor.remove_rev_success_job(self) + @pending_ancestor.add_rev_fail_project( @project, @os ) + + # remove the project that depends on me if exist + # and add it into rev_fail_project list if not exist + p_sub_jobs = @server.jobmgr.jobs.select do |j| + ( not j.pending_ancestor.nil? and + "#{j.pending_ancestor.id}" == "#{@pending_ancestor.id}" and + j.is_build_dependent_project(@project, @os) ) + end + p_sub_jobs.each do |d| + @pending_ancestor.remove_rev_success_job(d) + @pending_ancestor.add_rev_fail_project( d.get_project, d.os ) + + if not d.thread.nil? then d.thread.terminate end + d.status = "WAITING" + end + end + when "WORKING", "WAITING" , "INITIALIZING" , "JUST_CREATED" then + #just log + else # ERROR | FINISHED | RESOLVED + #do noting + end end # check building is possible def can_be_built_on?(host_os) - for pkg in @pkginfo.packages - if pkg.os == @os and pkg.build_host_os.include? host_os then + if @pkginfo.nil? then return false end + + @pkginfo.packages.each do |pkg| + if pkg.os_list.include? @os and pkg.build_host_os.include? host_os then return true end end - return false end + def get_packages() + return @pkginfo.packages + end + + + def get_build_dependencies(target_os) + return @pkginfo.get_build_dependencies(target_os) + end + + + def get_source_dependencies(target_os,host_os) + return @pkginfo.get_source_dependencies(target_os,host_os) + end + + + def is_compatible_with?(o) + if type != o.type then return false end + + my_project = get_project() + other_project = o.get_project() + + # check project name + if my_project.nil? or other_project.nil? or + my_project.name != other_project.name then + return false + end + + # check version + if @pkginfo.nil? or o.pkginfo.nil? or + not (Version.new(@pkginfo.get_version()) == Version.new(o.pkginfo.get_version())) then + return false + end + + # check compat os + @pkginfo.get_target_packages(@os).each do |p| + if not p.os_list.include?(o.os) then return false end + end + + return true + end + + def has_build_dependency?(other_job) if has_same_packages?(other_job) or - does_depend_on?(other_job) or - does_depended_by?(other_job) then + does_depend_on?(other_job) or + does_depended_by?(other_job) then return true else @@ -102,35 +356,53 @@ class BuildJob def has_same_packages?( wjob ) - for pkg in @pkginfo.packages - for wpkg in wjob.pkginfo.packages + + # same package must have same os + if not @os.eql? wjob.os then + return false + end + + # check package name + get_packages().each do |pkg| + wjob.get_packages().each do |wpkg| if pkg.package_name == wpkg.package_name then #puts "Removed from candiated... A == B" return true end end end + return false end def does_depend_on?( wjob ) - for dep in @pkginfo.get_build_dependencies(@os, BuildServer::HOST_OS) - for wpkg in wjob.pkginfo.packages - if dep.package_name == wpkg.package_name then + + # compare build dependency + get_build_dependencies(@os).each do |dep| + wjob.get_packages().each do |wpkg| + # dep packages of my job must have same name and target os + # with packages in working job + if dep.package_name == wpkg.package_name and + dep.target_os_list.include? wjob.os then #puts "Removed from candiated... A -> B" return true end end end + return false end def does_depended_by?( wjob ) - for pkg in @pkginfo.packages - for dep in wjob.pkginfo.get_build_dependencies(@os, BuildServer::HOST_OS) - if pkg.package_name == dep.package_name then + + get_packages().each do |pkg| + wjob.get_build_dependencies(wjob.os).each do |dep| + # dep package of working job must have same name and target os + # with packages in my job + if dep.package_name == pkg.package_name and + dep.target_os_list.include? @os then #puts "Checking... A <- B" return true end @@ -141,31 +413,135 @@ class BuildJob def is_connected? + return @log.is_connected? + end - # nil? then false - if @outstream.nil? then + + # return the job is asyncronous job + def is_asynchronous_job? + if not @log.has_second_out? then + return true + else return false end + end - # send chk signal - begin - BuildCommServer.send_chk( @outstream ) - rescue - return false + + # remove job from reverse success job + def remove_rev_success_job( job ) + @rev_success_jobs.delete job if @rev_success_jobs.include? job + end + + + # check [project,os] is in reverse fail project list + def is_rev_fail_project( prj, os ) + # check the project already exist + @rev_fail_projects.each do |p| + if p[0] == prj and p[1] == os then + return true + end end - return true + return false end - # return the job is asyncronous job - def is_asynchronous_job? - if @outstream.nil? then - return true + # add [project,os] to reverse fail project list + def add_rev_fail_project( prj, os ) + # check the project already exist + @rev_fail_projects.each do |p| + if p[0] == prj and p[1] == os then + return + end + end + # if not, add it + @rev_fail_projects.push [prj,os] + end + + + # remove [project,os] from reverse fail project list + def remove_rev_fail_project( prj, os ) + remove_list = [] + + # check project and os name + @rev_fail_projects.each do |p| + if p[0] == prj and p[1] == os then + remove_list.push p + end + end + + # remove + remove_list.each do |r| + @rev_fail_projects.delete r + end + end + + + # get project that my job is dependent on + def get_build_dependent_projects() + if @build_dep_prjs.nil? then + deps = @pkginfo.get_build_dependencies(@os) + pkgs = deps.map{|x| + # if "os" is not specified, use my "os" + if x.target_os_list.nil? or x.target_os_list.empty? then + os = @os + else + os = x.target_os_list[0] + end + + # package as item + @pkgsvr_client.get_pkg_from_list(x.package_name, os) + } + prjs = @server.prjmgr.get_projects_from_pkgs(pkgs) + @build_dep_prjs = prjs + end + + return @build_dep_prjs + end + + + # check if the project is my dependent project + def is_build_dependent_project( prj, os ) + dep_list = get_build_dependent_projects() + dep_list.each do |dep| + if dep[0] == prj and dep[1] == os then + return true + end + end + + return false + end + + + def progress + if not @log.nil? then + if @project.nil? or @project.get_latest_log_cnt.nil? then + return "--% (#{log.cnt.to_s} lines) " + else + return ( ( @log.cnt * 100 ) / @project.get_latest_log_cnt ).to_s + "%" + end + end + # if log is nil then can't figure progress out + return "" + end + + + def get_log_url() + # only when server support log url + if @server.job_log_url.empty? then + return "","" + end + + url = "#{@server.job_log_url}/#{@id}/log" + # if remote, the file existence must be checked + if File.exist? "#{@job_root}/remote_log" then + return url,"#{@server.job_log_url}/#{@id}/remote_log" else - return false + return url,"" end end + + # # PROTECTED METHODS # @@ -176,52 +552,70 @@ class BuildJob def thread_main @log.info( "New Job #{@id} is started", Log::LV_USER) - # update local package server - @server.local_pkgsvr.sync( @server.local_pkgsvr.get_default_dist_name(), false ) - - # checking version - if not check_package_version() - @status = "ERROR" - - return - end - # checking build dependency - if not check_build_dependency() + if not @is_remote_job and not @is_internal_job and + not check_build_dependency() then + if @is_internal_job then copy_result_files_to_master() end @status = "ERROR" - return + return false end # clean build - if not build() + if not build() then + if @is_internal_job then copy_result_files_to_master() end + @status = "ERROR" - return + return false end # upload - if not upload() + if not @is_rev_build_check_job and not @is_internal_job and + @parent.nil? and + not upload() then @status = "ERROR" - return + return false + end + + # copy result files to transport path + if @is_internal_job then + copy_result_files_to_master() + elsif not @parent.nil? and not @is_rev_build_check_job then + copy_result_files(@parent.source_path) end # INFO. don't change this string @log.info( "Job is completed!", Log::LV_USER) @status = "FINISHED" + return true end # check if local package version is greater than server - def check_package_version() + def check_package_version( source_info ) @log.info( "Checking package version ...", Log::LV_USER) - # package update - @pkgsvr_client.update + # check if version is same and source_info is different + ver_local = @pkginfo.packages[0].version + old_source_info = @project.get_source_info( ver_local ) + if not old_source_info.nil? and old_source_info != source_info then + @log.error( "Source code has been changed without increasing version!", Log::LV_USER) + @log.error( " * Version : #{ver_local}", Log::LV_USER) + @log.error( " * Before : #{old_source_info}", Log::LV_USER) + @log.error( " * Current : #{source_info}", Log::LV_USER) + + return false + end + + # compare with package version in package server + @pkginfo.packages.each do |pkg| + # check all supported os + ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") + # ignore if package does not exist + if ver_svr.nil? then next end - for pkg in @pkginfo.packages - ver_local = pkg.version - #ver_svr = @pkgsvr_client.get_package_version( pkg.package_name, @os ) - ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") - if not ver_svr.nil? and Version.new(ver_local) <= Version.new(ver_svr) then + # compare version + if Version.new(ver_local) < Version.new(ver_svr) or + ( not @force_rebuild and Version.new(ver_local) == Version.new(ver_svr) ) then @log.error( "Version must be increased : #{ver_local} <= #{ver_svr}", Log::LV_USER) return false end @@ -232,127 +626,422 @@ class BuildJob # build dependency version + # make sure that package server has all dependency packages of job def check_build_dependency() @log.info( "Checking build dependency ...", Log::LV_USER) + @pkgsvr_client.update + unmet_bdeps = [] + @pkginfo.get_build_dependencies( @os ).each do |dep| + # if parent exist, search parent source path first + # if not found, check package server + ver_svr = nil + if not @parent.nil? then + local_pkg = get_local_path_of_dependency( dep, @parent ) + if not local_pkg.nil? then + ver_svr = Utils.get_version_from_package_file( local_pkg ) + else + ver_svr = nil + end + end + if not ver_svr.nil? then next end - for dep in @pkginfo.get_build_dependencies( @os, @host_os ) - #ver_svr = @pkgsvr_client.get_package_version( dep.package_name, @os ) - if dep.target_os_list.count != 0 then - dep_target_os = dep.target_os_list[0] - else - dep_target_os = @os + if not remote_package_of_dependency_exist?(dep) then + unmet_bdeps.push dep end - ver_svr = @pkgsvr_client.get_attr_from_pkg( dep.package_name, dep_target_os, "version") + end - if ver_svr.nil? - @log.error( "The package \"#{dep.package_name}\" for build-dependency is not found}", Log::LV_USER) - return false + @log.info( "Checking install dependency ...", Log::LV_USER) + unmet_ideps = [] + @pkginfo.get_install_dependencies( @os ).each do |dep| + # if parent exist, search pkginfos for all sub jobs + # if not found, check package server + found = false + if not @parent.nil? and @parent.type == "MULTIBUILD" then + @parent.sub_jobs.each { |j| + os = (dep.target_os_list.empty?) ? @os : dep.target_os_list[0] + if j.pkginfo.pkg_exist?(dep.package_name, dep.base_version, os) then + found = true; break + end + } end + if found then next end - if not dep.match? ver_svr - @log.error( "Version for build-dependency in not matched : server version => #{ver_svr}", Log::LV_USER) - return false + if not remote_package_of_dependency_exist?(dep) then + unmet_ideps.push dep end - end - - return true + end + + # unmet dependencies found , report the errors + if not unmet_bdeps.empty? or not unmet_ideps.empty? then + @log.error( "Unmet dependency found!", Log::LV_USER) + unmet_bdeps.each { |d| + os = (d.target_os_list.empty?) ? @os : d.target_os_list[0] + @log.error( " * #{d.package_name}(#{os}) for build-dependency", Log::LV_USER) + } + unmet_ideps.each { |d| + os = (d.target_os_list.empty?) ? @os : d.target_os_list[0] + @log.error( " * #{d.package_name}(#{os}) for install-dependency", Log::LV_USER) + } + + return false + else + return true + end end # build clean def build() - if @resolve then - @log.info( "Resolving job...", Log::LV_USER) + + # check there are pending pacakges which wait for me + # it will return nil if not exist + # this process must be skip if it is sub-job + if not @is_rev_build_check_job and not @is_internal_job then + @server.cancel_lock.synchronize{ + @pending_ancestor = get_pending_ancestor_job() + } + end + + if not @pending_ancestor.nil? then + # resolve other pending job + resolve() + elsif @resolve then + # wait for being resolved by other jobs + # this condition must be placed after checking pending status + wait_resolve() else - @log.info( "Building job...", Log::LV_USER) - end + # build + build_normal() + end + end - # create builder - builder = Builder.create( "JB#{@id}", @pkgserver_url,@log.path ) - if builder.nil? - @log.error( "Creating job builder failed", Log::LV_USER) - return false + + # return pending job that wait for me + def get_pending_ancestor_job() + @server.jobmgr.get_pending_jobs.each do |job| + if job.is_rev_fail_project(@project,@os) then + return job + end + end + + return nil + end + + + # check whether build this job or not + # if not build, then return its compat pkgs list + def check_compatable_packages + compat_pkgs = [] # [ package name, os, local_path ] + + @pkginfo.get_target_packages(@os).each do |p| + # if package has only os then must build + if p.os_list.count <= 1 then return [] end + + compat_found = false + p.os_list.each do |o| + # check parent pkgs first + if not @parent.nil? then + parent_pkgs = Dir.glob("#{@parent.source_path}/#{p.package_name}_*_*.zip") + parent_pkgs.each do |lp| + lpname = Utils.get_package_name_from_package_file( lp ) + lver = Utils.get_version_from_package_file(lp) + los = Utils.get_os_from_package_file( lp ) + if lpname == p.package_name and o == los and lver == p.version then + compat_pkgs.push [p.package_name,o,lp] + compat_found = true + break + end + end + end + if compat_found then break end + + # check other package already in package server + ver_svr = @pkgsvr_client.get_attr_from_pkg( p.package_name, o, "version") + if not ver_svr.nil? and p.version.eql? ver_svr then + compat_pkgs.push [p.package_name,o,nil] + compat_found = true + break + end + end + + # if there is no compat pkgs for one pkg, then must build + if not compat_found then return [] end end - @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + + return compat_pkgs + end + + + def build_normal() + @log.info( "Started to build this job...", Log::LV_USER) + + # create builder + if @is_remote_job then + builder = RemoteBuilder.new("JB#{@id}", @remote_server, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Remote Server : #{@remote_server.ip}:#{@remote_server.port}" ) + @log.info( " - FTP Server : #{@server.ftp_addr}" ) + else + builder = Builder.create( "JB#{@id}", @pkgserver_url, @log.path, + "#{@buildroot_dir}", @server.build_cache_dir ) + if builder.nil? + @log.error( "Creating job builder failed", Log::LV_USER) + return false + end + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Package Server : #{@pkgserver_url}" ) + @log.info( " - Build Cache Path : #{@server.build_cache_dir}" ) + end + @log.info( " - Log Path : #{@log.path}" ) # set log output builder.log.close builder.log = @log - #make pending_pkg_dir_list - pending_pkg_dir_list = [] - ignore_rev_dep_build_list = [] - @pkginfo.packages.each do |i| - @server.jobmgr.get_pending_jobs.each do |pj| - if pj.rev_fail_list.include? i.package_name then - pending_pkg_dir_list.push pj.source_path - pending_pkg_dir_list += pj.rev_success_list.map {|pjs| pjs.source_path} - ignore_rev_dep_build_list = pj.rev_fail_list - break - end - end - if not pending_pkg_dir_list.empty? then break end - end - dependency_package_exist = (not pending_pkg_dir_list.empty?) + # if sub job, install dependent packages of parent-pkgs and not clean + use_clean = true + local_pkgs = [] + local_pkgs += @external_pkgs + if not @parent.nil? then + use_clean = false + # get local packages to install + deps = @pkginfo.get_build_dependencies(@os) + local_pkgs += get_local_paths_of_chained_dependencies( deps, @parent ) + end + local_pkgs.uniq! + + #compatable os support + compat_ok = true + compat_pkgs = check_compatable_packages + if compat_pkgs.size > 0 and not @is_rev_build_check_job then + # bring package from server for reverse check + compat_pkgs.each do |p| + pkg_name = p[0]; cos = p[1]; local_path = p[2] + + if not local_path.nil? then + ext = File.extname(local_path) + base_package_name= File.basename(local_path, "#{cos}#{ext}") + @log.info( "Copying compatible package:#{local_path}", Log::LV_USER) + @log.info( "Creating package file ... #{base_package_name}#{@os}#{ext}", Log::LV_USER) + FileUtils.cp local_path, "#{@source_path}/#{base_package_name}#{@os}#{ext}" + else + @log.info( "Downloading compatible package:#{pkg_name}(#{cos})", Log::LV_USER) + loc = @pkgsvr_client.download(pkg_name, cos, false) + if loc.nil? or loc.count != 1 then + @log.warn( "Downloading compatible package failed!:#{pkg_name}(#{cos})", Log::LV_USER) + compat_ok = false + break + end + ext = File.extname(loc[0]) + base_package_name= File.basename(loc[0], "#{cos}#{ext}") + @log.info( "Creating package file ... #{base_package_name}#{@os}#{ext}", Log::LV_USER) + FileUtils.mv loc[0], "#{@source_path}/#{base_package_name}#{@os}#{ext}" + end + end + else + compat_ok = false + end + + # if compat check failed + if not compat_ok then + # build + if @is_remote_job then + result = builder.build(@project.repository, @source_path, @os, + @is_rev_build_check_job, @git_commit, @no_reverse, local_pkgs) + else + result = builder.build(@source_path, @os, use_clean, local_pkgs, false ) + end + if not result then + @log.error( "Building job failed", Log::LV_USER) + write_log_url() + return false + end + end + + # check reverse dependecy if not sub jobs + + if not @no_reverse then + if not @is_rev_build_check_job and not @is_internal_job and + not ReverseBuildChecker.check( self, true ).empty? then + @log.error( "Reverse-build-check failed!" ) + return false + end + end + + return true + end + + + # wait to be resolved by other jobs + def wait_resolve() + @log.info( "Started to build this job and wait for being resolved...", Log::LV_USER) + + # create builder + if @is_remote_job then + builder = RemoteBuilder.new("JB#{@id}", @remote_server, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Remote Server : #{@remote_server.ip}:#{@remote_server.port}" ) + @log.info( " - FTP Server : #{@server.ftp_addr}" ) + else + builder = Builder.create( "JB#{@id}", @pkgserver_url, @log.path, + "#{@buildroot_dir}/#{@os}", @server.build_cache_dir ) + if builder.nil? + @log.error( "Creating job builder failed", Log::LV_USER) + return false + end + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Package Server : #{@pkgserver_url}" ) + @log.info( " - Build Cache Path : #{@server.build_cache_dir}" ) + end + @log.info( " - Log Path : #{@log.path}" ) + + # set log output + builder.log.close + builder.log = @log # build - if @resolve then - @rev_fail_list = builder.build_resolve(@source_path, @os, [], []) + if @is_remote_job then + result = builder.build(@project.repository, @source_path, @os, + false, @git_commit, @no_reverse, []) + else + result = builder.build(@source_path, @os, true, [], false ) + end + if not result then + @log.error( "Building job failed", Log::LV_USER) + write_log_url() + return false + end - # clean build failed - if @rev_fail_list.nil? then - @log.error( "Resolve building job failed", Log::LV_USER) - return false - end + # check reverse dependecy + @rev_fail_projects = ReverseBuildChecker.check(self, false) + if @rev_fail_projects.empty? then + # if no problem?, it OK + return true + end - # pending - @status = "PENDING" + # pending + @status = "PENDING" + @log.info( "Entered the PENDING state ...", Log::LV_USER) + old_msg = "" + while @status == "PENDING" + new_msg = @rev_fail_projects.map {|p| "#{p[0].name}(#{p[1]})"}.join(", ") + if old_msg != new_msg then + @log.error( " * Waiting for building next projects: #{new_msg}", Log::LV_USER) + old_msg = new_msg + end + sleep 1 + end - # rev build successed - if @rev_fail_list.empty? then - @rev_success_list.each do |s| - s.status = "" - end - @status = "" - end + return true + end - @log.info "Enters the PENGING state ..." - while @status == "PENDING" - sleep 1 - end - return true - else - if not builder.build(@source_path, @os, true, true, pending_pkg_dir_list, ignore_rev_dep_build_list ) - @log.error( "Building job failed", Log::LV_USER) - return false + + # resolve other pending job + def resolve() + + # wait for other build-dependent projects are resolved + old_msg = "" + wait_prjs = @pending_ancestor.rev_fail_projects.select {|p| is_build_dependent_project(p[0], p[1])} + @log.info("Checking build dependency before RESOLVE", Log::LV_USER) + while not wait_prjs.empty? + @status = "PENDING" + new_msg = wait_prjs.map {|p| "#{p[0].name}(#{p[1]})"}.join(", ") + if new_msg != old_msg then + @log.info(" * Waiting for building next projects: #{new_msg}", Log::LV_USER) + old_msg = new_msg + end + sleep 1 + wait_prjs = @pending_ancestor.rev_fail_projects.select {|p| is_build_dependent_project(p[0], p[1])} + end + + # return back to "WORKING" + @status = "WORKING" + + @log.info( "Started to build this job and resolve other pending job...", Log::LV_USER) + + # create builder + if @is_remote_job then + builder = RemoteBuilder.new("JB#{@id}", @remote_server, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Remote Server : #{@remote_server.ip}:#{@remote_server.port}" ) + @log.info( " - FTP Server : #{@server.ftp_addr}" ) + else + builder = Builder.create( "JB#{@id}", @pkgserver_url, @log.path, + "#{@buildroot_dir}/#{@os}", @server.build_cache_dir ) + if builder.nil? + @log.error( "Creating job builder failed", Log::LV_USER) + return false + end + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Package Server : #{@pkgserver_url}" ) + @log.info( " - Build Cache Path : #{@server.build_cache_dir}" ) + end + @log.info( " - Log Path : #{@log.path}" ) + + # set log output + builder.log.close + builder.log = @log + + # get local packages to overwite + # they must be composed of packages of pending jobs and its success list + local_pkgs=[] + local_pkgs += @external_pkgs + src_path = @pending_ancestor.source_path + ver = @pending_ancestor.pkginfo.get_version() + @pending_ancestor.pkginfo.get_target_packages(@os).each do |pkg| + local_pkgs.push "#{src_path}/#{pkg.package_name}_#{ver}_#{@os}.zip" + end + @pending_ancestor.rev_success_jobs.each do |job| + src_path = job.source_path + ver = job.pkginfo.get_version() + job.pkginfo.get_target_packages(@os).each do |pkg| + local_pkgs.push "#{src_path}/#{pkg.package_name}_#{ver}_#{@os}.zip" + end + end + + # build + if @is_remote_job then + result = builder.build(@project.repository, @source_path, @os, + false, @git_commit, @no_reverse, local_pkgs) + else + result = builder.build(@source_path, @os, true, local_pkgs, false ) + end + if not result then + @log.error( "Building job failed", Log::LV_USER) + write_log_url() + return false + end + + # check reverse dependecy and update parent rev_fail_project list + new_fail_projects = ReverseBuildChecker.check(self, false) + new_fail_projects.each do |p| + @pending_ancestor.add_rev_fail_project(p[0], p[1]) + end + + # update the status of pending job + @status = "PENDING" + @pending_ancestor.remove_rev_fail_project(@project, @os) + @pending_ancestor.rev_success_jobs.push self + if @pending_ancestor.rev_fail_projects.empty? then + @pending_ancestor.status = "RESOLVED" + @pending_ancestor.rev_success_jobs.each do |job| + job.status = "RESOLVED" end + else + @log.info( "Entered the PENDING state ...", Log::LV_USER) + old_msg = "" + while @status == "PENDING" + new_msg = @pending_ancestor.rev_fail_projects.map {|p| "#{p[0].name}(#{p[1]})"}.join(", ") + + if new_msg != old_msg then + @log.info(" * Waiting for building next projects: #{new_msg}", Log::LV_USER) + old_msg = new_msg + end - if dependency_package_exist then - @server.jobmgr.get_pending_jobs.each do |j| - if j.source_path == pending_pkg_dir_list[0] then - j.rev_fail_list -= @pkginfo.packages.map{|p| p.package_name} - j.rev_success_list.push self - if j.rev_fail_list.empty? then - j.rev_success_list.each do |s| - s.status = "" - end - j.status = "" - else - @status = "PENDING" - @log.info "Enters the PENGING state ..." - while @status == "PENDING" - sleep 1 - end - end - break - end - end - end + sleep 1 + end end - # remove builder - Builder.remove( "builder_#{@id}" ) - return true end @@ -362,13 +1051,10 @@ class BuildJob # get package path list binpkg_path_list = Dir.glob("#{@source_path}/*_*_#{@os}.zip") - srcpkg_path_list = Dir.glob("#{@source_path}/*.tar.gz") # upload u_client = Client.new( @server.pkgserver_url, nil, @log ) - u_client.update - snapshot = u_client.upload( @server.pkgserver_addr, - @server.pkgserver_id, binpkg_path_list, srcpkg_path_list, true) + snapshot = u_client.upload( @server.pkgserver_addr, @server.pkgserver_port, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd, binpkg_path_list) if snapshot.nil? then @log.info( "Upload failed...", Log::LV_USER) @@ -379,46 +1065,157 @@ class BuildJob # update local @log.info( "Upload succeeded. Sync local pkg-server again...", Log::LV_USER) @pkgsvr_client.update - @server.local_pkgsvr.sync( @server.local_pkgsvr.get_default_dist_name(), false ) @log.info("Snapshot: #{snapshot}", Log::LV_USER) return true end - # remote main module - def remote_thread_main(server) - @log.info( "Job #{@id} is requested to be built on remote server ", Log::LV_USER) - - # open - client = BuildCommClient.create( server.ip, server.port ) - if client.nil? then - @status = "ERROR" - return + def copy_result_files(dst_path) + @log.info( "Copying result files to #{dst_path}", Log::LV_USER) + + # get package path list + binpkg_path_list = Dir.glob("#{@source_path}/*_*_#{@os}.zip") + + binpkg_path_list.each do |file| + @log.info( " * #{file}", Log::LV_USER) + FileUtils.cp(file,"#{dst_path}/") end - - # send & receive - if client.send("BUILD,GIT,#{@git_repos},#{@git_commit},#{@os},,NO") then - result = client.read_lines do |l| - if l.include? "Job is stopped by ERROR" then - @status = "ERROR" - end - # ddd list - @log.output( l.strip, Log::LV_USER) + + return true + end + + + # copy binary package files and log file to transport dir + def copy_result_files_to_master() + outgoing_dir = "#{@server.transport_path}/#{@dock_num}" + + @log.info( "Copying log to #{outgoing_dir}", Log::LV_USER) + file = "#{@source_path}/../log" + FileUtils.copy_file(file, "#{outgoing_dir}/remote_log") + + # copy result files, if not reverse build + if not @is_rev_build_check_job then + return copy_result_files( outgoing_dir ) + else + return true + end + end + + + protected + def get_local_path_of_dependency( dep, parent ) + dep_target_os = get_os_of_dependency(dep) + + # search my parent job and its parent job + binpkgs = Dir.glob("#{parent.source_path}/#{dep.package_name}_*_#{dep_target_os}.zip") + if binpkgs.count == 0 and not parent.get_parent_job().nil? then + binpkgs = Dir.glob("#{parent.get_parent_job().source_path}/#{dep.package_name}_*_#{dep_target_os}.zip") + end + + if binpkgs.count > 0 then + pkg = binpkgs[0] + version = Utils.get_version_from_package_file(pkg) + if dep.match? version then + return pkg + else + return nil end - if not result then @status = "ERROR" end + else + return nil end + end - # close socket - client.terminate - # INFO. don't change this string - if @status != "ERROR" then - @log.info( "Job is just finished", Log::LV_USER) - @status = "FINISHED" + protected + def get_local_paths_of_chained_dependencies( deps, parent ) + pkg_paths = [] + + # get packages names that is related my build dependency + chained_deps = get_local_chained_dependencies( deps, parent ) + + # get all local path of dependencies + chained_deps.each { |dep| + new_path = get_local_path_of_dependency(dep, parent) + if not new_path.nil? then + pkg_paths.push new_path + end + } + + # remove duplicates + pkg_paths.uniq! + + return pkg_paths + end + + + protected + def get_local_chained_dependencies( deps, parent ) + + chained_deps = [] + chained_deps += deps + + # if parent is multi build job, gether all install dependency of dependency. + if parent.type == "MULTIBUILD" then + begin + old_deps_count = chained_deps.count + new_deps = [] + chained_deps.each { |dep| + dep_target_os = get_os_of_dependency(dep) + + parent.sub_jobs.each { |j| + new_deps += j.pkginfo.get_install_dependencies(dep_target_os, dep.package_name) + } + } + chained_deps += new_deps + chained_deps.uniq! {|d| d.package_name } + end while chained_deps.count != old_deps_count + end + + # check parent of parent + if not parent.get_parent_job().nil? then + chained_deps = get_local_chained_dependencies(chained_deps, parent.get_parent_job()) end - return + return chained_deps + end + + + protected + def remote_package_of_dependency_exist?(dep) + dep_target_os = get_os_of_dependency(dep) + + # search + ver_svr = @pkgsvr_client.get_attr_from_pkg( dep.package_name, dep_target_os, "version") + if ver_svr.nil? then return false end + if not dep.match? ver_svr then return false end + + return true + end + + + # write web url for log + protected + def write_log_url() + url,remote_url = get_log_url() + if not url.empty? then + @log.info( " ** Log1: #{url}", Log::LV_USER) + end + if not remote_url.empty? then + @log.info( " ** Log2: #{remote_url}", Log::LV_USER) + end end + + # get target os of dependency + protected + def get_os_of_dependency(dep) + # use the target os if not specified + if dep.target_os_list.count != 0 then + dep_target_os = dep.target_os_list[0] + else + dep_target_os = @os + end + end + end diff --git a/src/build_server/BuildServer.rb b/src/build_server/BuildServer.rb index d1d993d..0c7e3c6 100644 --- a/src/build_server/BuildServer.rb +++ b/src/build_server/BuildServer.rb @@ -28,36 +28,52 @@ Contributors: require 'fileutils' $LOAD_PATH.unshift File.dirname(__FILE__) -$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" require "SocketJobRequestListener.rb" require "RemoteBuildJob.rb" -require "LocalBuildJob.rb" -require "packageServer.rb" require "JobManager.rb" +require "JobClean.rb" require "RemoteBuildServer.rb" +require "PackageSync.rb" +require "ProjectManager.rb" class BuildServer - attr_accessor :id, :path, :pkgserver_url, :pkgserver_addr, :pkgserver_id, :port, :status, :friend_servers, :host_os, :log + attr_accessor :id, :path, :pkgserver_url, :pkgserver_addr, :pkgserver_port, :pkgserver_id, :port, :status, :friend_servers, :host_os, :log attr_accessor :git_server_url, :git_bin_path attr_accessor :job_log_url attr_accessor :allowed_git_branch - attr_accessor :pkgsvr_cache_path, :local_pkgsvr attr_accessor :send_mail attr_accessor :jobmgr attr_accessor :test_time attr_accessor :password attr_accessor :finish + attr_accessor :build_cache_dir + attr_accessor :keep_time + attr_accessor :ftp_addr + attr_accessor :ftp_port + attr_accessor :ftp_username + attr_accessor :ftp_passwd + attr_accessor :cleaner + attr_accessor :prjmgr + attr_accessor :transport_path + attr_accessor :cancel_lock + attr_accessor :supported_os_list + attr_accessor :upgrade + attr_accessor :remote_pkg_servers + attr_accessor :pkg_sync_period + CONFIG_ROOT = "#{Utils::HOME}/.build_tools/build_server" HOST_OS = Utils::HOST_OS # initialize - def initialize (id, path, pkgserver_url, pkgserver_addr, pkgserver_id) + def initialize (id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_port, pkgsvr_id, ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd) @id = id @path = path - @pkgserver_url = pkgserver_url - @pkgserver_addr = pkgserver_addr - @pkgserver_id = pkgserver_id + @pkgserver_url = pkgsvr_url + @pkgserver_addr = pkgsvr_addr + @pkgserver_port = pkgsvr_port + @pkgserver_id = pkgsvr_id @friend_servers = [] + @remote_pkg_servers = [] @req_listener = [] @finish = false # port number @@ -75,11 +91,24 @@ class BuildServer @send_mail = "NO" # local package server @pkgsvr_cache_path = nil - @local_pkgsvr = nil # Job Manager @jobmgr = JobManager.new(self) @test_time=0 #test time in mili-seconds @password="0000" + @keep_time=86400 + @ftp_addr = ftpsvr_addr + @ftp_port = ftpsvr_port + @ftp_username = ftpsvr_username + @ftp_passwd = ftpsvr_passwd + @cleaner=nil + @prjmgr = ProjectManager.new(self) + # + @transport_path = "#{@path}/transport" + @cancel_lock = Mutex.new + @supported_os_list = [] + + @pkg_sync_period=600 + @upgrade = false end @@ -88,57 +117,74 @@ class BuildServer # start @log = Log.new( "#{BuildServer::CONFIG_ROOT}/#{@id}/log" ) - # set local package server for cache - @log.info "Setting local package server..." - pkgsvr_id = @id - pkgsvr_dist = @pkgserver_url.split("/")[-1] - @local_pkgsvr = PackageServer.new( pkgsvr_id ) - if @local_pkgsvr.location.empty? then - FileUtils.mkdir_p @pkgsvr_cache_path - @local_pkgsvr.create(pkgsvr_id, pkgsvr_dist, @pkgserver_url, @pkgsvr_cache_path ) - else - # check path is changed, recreate it - if @local_pkgsvr.location != "#{@pkgsvr_cache_path}/#{pkgsvr_id}" then - # remove - @local_pkgsvr.remove_server( pkgsvr_id ) - # create - FileUtils.mkdir_p @pkgsvr_cache_path - @local_pkgsvr.create(pkgsvr_id, pkgsvr_dist, @pkgserver_url, @pkgsvr_cache_path ) - end + # set build cache dir + @build_cache_dir="#{BuildServer::CONFIG_ROOT}/#{@id}/build_cache" + if not File.exist? @build_cache_dir then + FileUtils.mkdir_p @build_cache_dir end + # init transport path + if not File.exist? @transport_path then FileUtils.mkdir_p @transport_path end + + # init project mgr + @log.info "Setting Project Manager..." + @prjmgr.load() + + # init job mgr + @log.info "Intializing Job Manager..." + @jobmgr.init() + # set job request listener @log.info "Setting listener..." listener2 = SocketJobRequestListener.new(self) listener2.start @req_listener.push listener2 + + # set job cleaner + @log.info "Setting Job Cleaner..." + @cleaner = JobCleaner.new(self) + @cleaner.start + + # set package server synchrontizer + if not @remote_pkg_servers.empty? then + @log.info "Setting Package Server Synchronizer..." + @pkg_sync = PackageServerSynchronizer.new(self) + @pkg_sync.start + end # main loop @log.info "Entering main loop..." - if @test_time > 0 then start_time = Time.now end - while( not @finish ) - - # update friend server status - for server in @friend_servers - # update state - server.update_state - end - - # handle jobs - @jobmgr.handle() - - # sleep - if @test_time > 0 then - curr_time = Time.now - if (curr_time - start_time).to_i > @test_time then - puts "Test time is elapsed!" - break + begin + if @test_time > 0 then start_time = Time.now end + while( not @finish ) + + # update friend server status + @friend_servers.each do |server| + # update state + server.update_state + end + + # handle jobs + @jobmgr.handle() + + # sleep + if @test_time > 0 then + curr_time = Time.now + if (curr_time - start_time).to_i > @test_time then + puts "Test time is elapsed!" + break + end + else + sleep 1 end - else - sleep 1 end + rescue => e + @log.error( e.message, Log::LV_USER) end + if(@upgrade) + exit(99) + end # TODO: something should be done for server down end @@ -152,6 +198,10 @@ class BuildServer # check the job can be built on this server def can_build?(job) + # check max allowed jobs + if @jobmgr.max_working_jobs <= 0 then + return false + end # check me if job.can_be_built_on? @host_os then @@ -166,39 +216,78 @@ class BuildServer def add_remote_server( ip, port ) # if already exit, return false - for svr in @friend_servers + @friend_servers.each do |svr| if svr.ip.eql? ip and svr.port == port then return false end end # create new one, and add it into list - new_server = RemoteBuildServer.new( ip, port ) + new_server = RemoteBuildServer.new( ip, port, self ) @friend_servers.push new_server return true end + # add new remote pkg server + def add_remote_package_server( url, proxy ) + + # if already exit, return false + @remote_pkg_servers.each do |entry| + u = entry[0] + + if u == url then + return false + end + end + + @remote_pkg_servers.push [url, proxy] + + return true + end + + + # add new target OS. + # If already exist, return false , otherwise true + def add_target_os( os_name ) + + # if already exit, return false + @supported_os_list.each do |os| + if os.eql? os_name then + return false + end + end + + # add it into list + @supported_os_list.push os_name + + return true + end + + # get remote server def get_available_server ( job ) candidates = [] - # check local - if @jobmgr.get_number_of_empty_room > 0 and can_build?(job) then + # calculate empty rooms + # if sub job, his parent should be excluded + local_empty_rooms = @jobmgr.get_number_of_empty_room + + if local_empty_rooms > 0 and can_build?(job) then candidates.push self end - # if Local build job, just check local - if job.instance_of? LocalBuildJob then return candidates[0] end - - # get availables - for server in @friend_servers - # select only "RUNNING" & possible one - if ( server.status == "RUNNING" and server.can_build?( job ) and - not server.has_waiting_jobs and - server.get_number_of_empty_room > 0 ) - candidates.push server + # get availables server + # but, job must not be "REGISTER" and "MULTIBUILD" job + if job.type != "REGISTER" and job.type != "MULTIBUILD" then + @friend_servers.each do |server| + if ( server.status == "RUNNING" and server.can_build?( job ) and + not server.has_waiting_jobs and + server.get_file_transfer_cnt() == 0 and + server.get_number_of_empty_room > 0 ) + candidates.push server + end end end @@ -208,7 +297,7 @@ class BuildServer # get best # it is better if working jobs count is less max_empty_room = best_server.get_number_of_empty_room - for server in candidates + candidates.each do |server| # check whether idle, use it if not server.has_working_jobs then return server end @@ -234,7 +323,7 @@ class BuildServer if can_build? job then return true end #if not found, check friends - for server in @friend_servers + @friend_servers.each do |server| if server.status == "RUNNING" and job.can_be_built_on? server.host_os then return true @@ -261,5 +350,6 @@ class BuildServer def has_waiting_jobs return @jobmgr.has_waiting_jobs end + end diff --git a/src/build_server/BuildServerController.rb b/src/build_server/BuildServerController.rb index a58734d..4be6726 100644 --- a/src/build_server/BuildServerController.rb +++ b/src/build_server/BuildServerController.rb @@ -34,7 +34,7 @@ class BuildServerController @@instance_map = {} # create - def self.create_server (id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_id) + def self.create_server (id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_port, pkgsvr_id, ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd) # check server config root check_build_server_root @@ -45,11 +45,11 @@ class BuildServerController end # create new instance and return it - @@instance_map[id] = BuildServer.new( id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_id ) + @@instance_map[id] = BuildServer.new( id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_port, pkgsvr_id, ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd) # set default @@instance_map[id].git_server_url="gerrithost:" - if Utils::HOST_OS == "windows" then + if Utils.is_windows_like_os(Utils::HOST_OS) then @@instance_map[id].git_bin_path="/c/Program\\ Files/Git/bin/git.exe" else @@instance_map[id].git_bin_path="/usr/bin/git" @@ -58,7 +58,6 @@ class BuildServerController @@instance_map[id].jobmgr.max_working_jobs= 2 @@instance_map[id].job_log_url="" @@instance_map[id].send_mail="NO" - @@instance_map[id].pkgsvr_cache_path="#{path}/pkgsvr_cache" # write config @@ -79,12 +78,9 @@ class BuildServerController FileUtils.rm_rf "#{BuildServer::CONFIG_ROOT}/#{id}" @@instance_map[id] = nil puts "Removed the server \"#{id}\"" + else + puts "The server \"#{id}\" does not exist!" end - - # remove local package server - local_pkgsvr = PackageServer.new( id ) - local_pkgsvr.remove_server(id) - end @@ -98,7 +94,7 @@ class BuildServerController # check server config if not File.exist? "#{BuildServer::CONFIG_ROOT}/#{id}/server.cfg" - raise RuntimeError, "The server \"#{id}\" does not exist." + raise RuntimeError, "The server \"#{id}\" does not exist!" end # get server config and return its object @@ -137,7 +133,7 @@ class BuildServerController # send request stop_ok = false - if client.send "STOP,#{server.password}" then + if client.send "STOP|#{server.password}" then # recevie & print mismatched = false result = client.read_lines do |l| @@ -161,6 +157,94 @@ class BuildServerController return true end + # upgrade server + def self.upgrade_server( id ) + + # server + server = get_server(id) + client = BuildCommClient.create( "127.0.0.1", server.port ) + if client.nil? then + puts "Server is not running!" + return false + end + + # send request + upgrade_ok = false + if client.send "UPGRADE|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + upgrade_ok = true + end + end + + # terminate + client.terminate + + if not upgrade_ok then + puts "Server upgrade failed!" + end + + return true + end + + # request upgrade friends build server + def self.request_upgrade_server( id ) + + server = get_server(id) + server_dir = "#{BuildServer::CONFIG_ROOT}/#{id}" + + if File.exist? "#{server_dir}/friends" then + File.open( "#{server_dir}/friends", "r" ) do |f| + f.each_line do |l| + if l.split(",").count < 2 then next end + ip = l.split(",")[0].strip + port = l.split(",")[1].strip + + client = BuildCommClient.create( ip, port ) + if client.nil? then + puts "Friend Server #{ip}:#{port} is not running!" + next + end + # send request + upgrade_ok = false + if client.send "UPGRADE|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + upgrade_ok = true + end + end + + # terminate + client.terminate + + if upgrade_ok then + puts "Friend Server #{ip}:#{port} upgrade requested!" + else + puts "Friend Server #{ip}:#{port} upgrade failed!" + end + end + end + else + puts "No Friend Server." + end + + return true + end + # add friend server def self.add_friend_server( id, ip, port ) @@ -185,82 +269,210 @@ class BuildServerController end - # build git repository and upload - def self.build_git( id, repository, commit, os, url, resolve ) - - # server + # add remote package server + def self.add_remote_package_server(id, url, proxy ) server = get_server(id) - client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + + # add + if server.add_remote_package_server( url, proxy ) then - # send request - client.send "BUILD,GIT,#{repository},#{commit},#{os}" + # write config + server_dir = "#{BuildServer::CONFIG_ROOT}/#{server.id}" + f = File.open( "#{server_dir}/remote_pkg_servers", "a" ) + if not proxy.nil? then + f.puts "#{url}|#{proxy}" + else + f.puts "#{url}|" + end + f.close + + puts "Remote package server is added!" + + return true + else + puts "The server already exists in list!" - # recevie & print - client.print_stream + return false + end + end - # terminate - client.terminate - return true + # add supported target os + def self.add_target_os( id, os_name ) + # TODO:check os foramt + if os_name == "default" then + puts "Cannot use \"default\" as target OS name!" + return false + end + + # get server + server = get_server(id) + + # add + if server.add_target_os( os_name ) then + + # write config + server_dir = "#{BuildServer::CONFIG_ROOT}/#{server.id}" + f = File.open( "#{server_dir}/supported_os_list", "a" ) + f.puts "#{os_name}" + f.close + + puts "Target OS is added successfully!" + + return true + else + puts "Target OS already exists in list!" + return false + end end - # resolve git and build it and upload - def resolve_git( id, repository, commit, os, url ) - # server + # add project + def self.add_project( id, project_name, git_repos, git_branch, remote_server_id, passwd, os_string ) + # get server server = get_server(id) - client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + + # get supported os for project. + # if not specified, all supported os of the server will be used + if os_string.nil? or os_string == "default" then + os_list = server.supported_os_list + else + os_list = os_string.strip.split(",") + end + + # check OS name + os_list.each do |os| + if not server.supported_os_list.include? os then + puts "Unsupported OS name \"#{os}\" is used!" + puts "Check the following supported OS list:" + server.supported_os_list.each do |s_os| + puts " * #{s_os}" + end - # send request - client.send "RESOLVE,GIT,#{repository},#{commit},#{os}" + return false + end + end - # recevie & print - client.print_stream + # add + if not git_repos.nil? and not git_branch.nil? then + result = server.prjmgr.add_git_project( project_name, git_repos, git_branch, passwd, os_list ) + elsif not remote_server_id.nil? then + result = server.prjmgr.add_remote_project( project_name, remote_server_id, passwd, os_list) + else + result = false + end + + if result then + puts "Adding project succeeded!" + return true + else + puts "Adding project failed!" + return false + end + end - # terminate - client.terminate - return true + # add binary project + def self.add_binary_project( id, project_name, pkg_name, passwd, os_string ) + # get server + server = get_server(id) + + # get supported os for project. + # if not specified, all supported os of the server will be used + if os_string.nil? or os_string == "default" then + os_list = server.supported_os_list + else + os_list = os_string.strip.split(",") + end + + # add + result = server.prjmgr.add_binary_project( project_name, pkg_name, passwd, os_list ) + + if result then + puts "Adding project succeeded!" + return true + else + puts "Adding project failed!" + return false + end end - # build local project and upload - def self.build_local( id, local_path, os, url, resolve ) + # full build + def self.build_all_projects( id ) + # server server = get_server(id) client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + if client.nil? then + puts "Server is not running!" + return false + end # send request - client.send "BUILD,LOCAL,#{local_path},#{os}" - - # recevie & print - client.print_stream + fullbuild_ok = false + if client.send "FULLBUILD|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + fullbuild_ok = true + end + end # terminate client.terminate + + if not fullbuild_ok then + puts "Full build failed!" + end return true end - # resolve local project and build it and upload - def resolve_local( path, os ) + def self.register_package(id, file_path) # server server = get_server(id) client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + if client.nil? then + puts "Server is not running!" + return false + end - # send request - client.send "RESOLVE,LOCAL,#{local_path},#{os}" + if not File.exist? file_path then + puts "File not found!" + return false + end - # recevie & print - client.print_stream + file_path = File.expand_path(file_path) + # send request + success = false + if client.send "REGISTER|BINARY-LOCAL|#{file_path}|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + success = true + end + end # terminate client.terminate + + if not success then + puts "Registering package failed!" + end return true end @@ -281,7 +493,9 @@ class BuildServerController def self.write_server_config( server ) # create config folder server_dir = "#{BuildServer::CONFIG_ROOT}/#{server.id}" - FileUtils.mkdir_p( "#{server_dir}" ) + if not File.exist? server_dir then + FileUtils.mkdir_p( server_dir ) + end # write configuration File.open( "#{server_dir}/server.cfg", "w" ) do |f| @@ -289,8 +503,8 @@ class BuildServerController f.puts "PATH=#{server.path}" f.puts "PSERVER_URL=#{server.pkgserver_url}" f.puts "PSERVER_ADDR=#{server.pkgserver_addr}" + f.puts "PSERVER_PORT=#{server.pkgserver_port}" f.puts "PSERVER_ID=#{server.pkgserver_id}" - f.puts "PSERVER_CACHE_PATH=#{server.pkgsvr_cache_path}" f.puts "GIT_SERVER_URL=#{server.git_server_url}" f.puts "GIT_BIN_PATH=#{server.git_bin_path}" f.puts "ALLOWED_GIT_BRANCH=#{server.allowed_git_branch}" @@ -299,6 +513,12 @@ class BuildServerController f.puts "SEND_MAIL=#{server.send_mail}" f.puts "TEST_TIME=#{server.test_time}" if server.test_time > 0 f.puts "PASSWORD=#{server.test_time}" if server.password != "0000" + f.puts "JOB_KEEP_TIME=#{server.keep_time}" + f.puts "FTP_ADDR=#{server.ftp_addr}" + f.puts "FTP_PORT=#{server.ftp_port}" + f.puts "FTP_USERNAME=#{server.ftp_username}" + f.puts "FTP_PASSWD=#{server.ftp_passwd}" + f.puts "PKG_SYNC_PERIOD=#{server.pkg_sync_period}" end end @@ -308,8 +528,8 @@ class BuildServerController path="" pkgsvr_url="" pkgsvr_addr="" + pkgsvr_port="3333" pkgsvr_id="" - pkgsvr_cache_path="" git_server_url="gerrithost:" git_bin_path="/usr/bin/git" allowed_git_branch="" @@ -318,6 +538,12 @@ class BuildServerController send_mail="NO" test_time=0 password="0000" + keep_time=86400 + ftp_addr="" + ftp_port="21" + ftp_username="" + ftp_passwd="" + pkg_sync_period=600 # read configuration server_dir = "#{BuildServer::CONFIG_ROOT}/#{id}" @@ -332,10 +558,10 @@ class BuildServerController pkgsvr_url = l[idx,length].strip elsif l.start_with?("PSERVER_ADDR=") pkgsvr_addr = l[idx,length].strip + elsif l.start_with?("PSERVER_PORT=") + pkgsvr_port = l[idx,length].strip elsif l.start_with?("PSERVER_ID=") pkgsvr_id = l[idx,length].strip - elsif l.start_with?("PSERVER_CACHE_PATH=") - pkgsvr_cache_path = l[idx,length].strip elsif l.start_with?("GIT_SERVER_URL=") git_server_url = l[idx,length].strip elsif l.start_with?("GIT_BIN_PATH=") @@ -352,6 +578,18 @@ class BuildServerController test_time = l[idx,length].strip.to_i elsif l.start_with?("PASSWORD=") password = l[idx,length].strip.to_i + elsif l.start_with?("JOB_KEEP_TIME=") + keep_time = l[idx,length].strip.to_i + elsif l.start_with?("FTP_ADDR=") + ftp_addr = l[idx,length].strip + elsif l.start_with?("FTP_PORT=") + ftp_port = l[idx,length].strip + elsif l.start_with?("FTP_USERNAME=") + ftp_username = l[idx,length].strip + elsif l.start_with?("FTP_PASSWD=") + ftp_passwd = l[idx,length].strip + elsif l.start_with?("PKG_SYNC_PERIOD=") + pkg_sync_period = l[idx,length].strip.to_i else next end @@ -359,7 +597,7 @@ class BuildServerController end # create server object - obj = BuildServer.new( id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_id ) + obj = BuildServer.new( id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_port, pkgsvr_id, ftp_addr, ftp_port, ftp_username, ftp_passwd ) # check running port if File.exist? "#{server_dir}/run" then @@ -381,6 +619,28 @@ class BuildServerController end end + # check remote package server + if File.exist? "#{server_dir}/remote_pkg_servers" then + File.open( "#{server_dir}/remote_pkg_servers", "r" ) do |f| + f.each_line do |l| + if l.split("|").count < 2 then next end + url = l.split("|")[0].strip + proxy = l.split("|")[1].strip + obj.add_remote_package_server( url, proxy ) + end + end + end + + # check supported os + if File.exist? "#{server_dir}/supported_os_list" then + File.open( "#{server_dir}/supported_os_list", "r" ) do |f| + f.each_line do |l| + os_name = l.strip + obj.add_target_os( os_name ) + end + end + end + # set git server url obj.git_server_url = git_server_url @@ -399,16 +659,24 @@ class BuildServerController # set allowed git branch name obj.allowed_git_branch = allowed_git_branch - # set package server path - pkgsvr_cache_path = (pkgsvr_cache_path.empty? ? "#{path}/pkgsvr_cache":pkgsvr_cache_path) - obj.pkgsvr_cache_path= pkgsvr_cache_path - # set test time obj.test_time = test_time # set password obj.password = password + # set password + obj.keep_time = keep_time + + # set ftp infomation + obj.ftp_addr = ftp_addr + obj.ftp_port = ftp_port + obj.ftp_username = ftp_username + obj.ftp_passwd = ftp_passwd + + # pkg synchronization + obj.pkg_sync_period = pkg_sync_period + # save config write_server_config( obj ) diff --git a/src/build_server/BuildServerOptionParser.rb b/src/build_server/BuildServerOptionParser.rb index f14f69c..4c1ba9a 100644 --- a/src/build_server/BuildServerOptionParser.rb +++ b/src/build_server/BuildServerOptionParser.rb @@ -26,56 +26,192 @@ Contributors: - S-Core Co., Ltd =end +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" require 'optparse' +require 'utils' + +def option_error_check( options ) + case options[:cmd] + + when "create" + if options[:name].nil? or options[:name].empty? or + options[:url].nil? or options[:url].empty? or + options[:domain].nil? or options[:domain].empty? or + options[:fdomain].nil? or options[:fdomain].empty? then + raise ArgumentError, "Usage: build-svr create -n -u -d -t " + end + + when "remove" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: build-svr remove -n " + end + + when "start" + if options[:name].nil? or options[:name].empty? or + options[:port].nil? then + raise ArgumentError, "Usage: build-svr start -n -p " + end + + when "stop" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: build-svr stop -n " + end + + when "upgrade" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: build-svr upgrade -n " + end + + when "add-svr" + if options[:name].nil? or options[:name].empty? or + ((options[:domain].nil? or options[:domain].empty?) and + (options[:url].nil? or options[:url].empty?)) then + raise ArgumentError, "Usage: build-svr add-svr -n (-d |-u ) [--proxy ]" + end + + when "add-prj" + if options[:name].nil? or options[:name].empty? or + options[:pid].nil? or options[:pid].empty? then + raise ArgumentError, "Usage: build-svr add-prj -n -N (-g -b |-P ) [-w ] [-o ]" + end + + when "add-os" + if options[:name].nil? or options[:name].empty? or + options[:os].nil? or options[:os].empty? then + raise ArgumentError, "Usage: build-svr add-os -n -o " + end + + when "fullbuild" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: build-svr fullbuild -n " + end + + when "register" + if options[:name].nil? or options[:name].empty? or + options[:package].nil? or options[:package].empty? then + raise ArgumentError, "Usage: build-svr register -n -P " + end + else + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" + end +end def option_parse options = {} - banner = "Usage: build-svr {create|remove|start|build|help} ..." + "\n" \ - + "\t" + "build-svr create -n -u -d -i " + "\n" \ - + "\t" + "build-svr remove -n " + "\n" \ - + "\t" + "build-svr start -n [-p " + "\n" \ - + "\t" + "build-svr add -n [-d -p ]" + "\n" - - optparse = OptionParser.new do|opts| + banner = "Build-server administer service command-line tool." + "\n" \ + + "\n" + "Usage: build-svr [OPTS] or build-svr (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "create Create the build-server." + "\n" \ + + "\t" + "remove Remove the build-server." + "\n" \ + + "\t" + "start Start the build-server." + "\n" \ + + "\t" + "stop Stop the build-server." + "\n" \ + + "\t" + "upgrade Upgrade the build-server include friends." + "\n" \ + + "\t" + "add-svr Add remote build/package server for support multi-OS or distribute build job." + "\n" \ + + "\t" + "add-prj Register information for project what you want build berfore building a project." + "\n" \ + + "\t" + "register Register the package to the build-server." + "\n" \ + + "\t" + "fullbuild Build all your projects and upload them to package server." + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + "build-svr create -n -u -d -t " + "\n" \ + + "\t" + "build-svr remove -n " + "\n" \ + + "\t" + "build-svr start -n -p " + "\n" \ + + "\t" + "build-svr stop -n " + "\n" \ + + "\t" + "build-svr upgrade -n " + "\n" \ + + "\t" + "build-svr add-svr -n (-d |-u ) [--proxy ]" + "\n" \ + + "\t" + "build-svr add-prj -n -N (-g -b |-P ) [-w ] [-o ]" + "\n" \ + + "\t" + "build-svr add-os -n -o " + "\n" \ + + "\t" + "build-svr register -n -P " + "\n" \ + + "\t" + "build-svr fullbuild -n " + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| # Set a banner, displayed at the top # of the help screen. opts.banner = banner - opts.on( '-n', '--name ', 'build server name' ) do|name| + opts.on( '-n', '--name ', 'build server name' ) do|name| options[:name] = name end - opts.on( '-u', '--url ', 'package server URL: http://xxx/yyy/zzz' ) do|url| + opts.on( '-u', '--url ', 'package server url: http://127.0.0.1/dibs/unstable' ) do|url| options[:url] = url end - opts.on( '-d', '--domain ', 'package svr or friend svr ip or ssh alias' ) do|domain| + options[:proxy] = nil + opts.on( '--proxy ', 'proxy url: http://172.21.111.100:2222' ) do|proxy| + options[:proxy] = proxy + end + + opts.on( '-d', '--address ', 'server address: 127.0.0.1:2224' ) do|domain| options[:domain] = domain end - opts.on( '-i', '--id ', 'package server id' ) do|pid| + options[:port] = 2222 + opts.on( '-p', '--port ', 'server port number: 2224' ) do|port| + options[:port] = port.strip.to_i + end + + opts.on( '-P', '--pkg ', 'package file path or name' ) do|package| + options[:package] = package.strip + end + + options[:os] = nil + opts.on( '-o', '--os ', 'ex) ubuntu-32,windows-32' ) do|os| + if not Utils.multi_argument_test( os, "," ) then + raise ArgumentError, "OS variable parsing error : #{os}" + end + options[:os] = os + end + + opts.on( '-N', '--pname ', 'project name' ) do|pid| options[:pid] = pid end - options[:port] = 2222 - opts.on( '-p', '--port ', 'port' ) do|port| - options[:port] = port.strip.to_i + opts.on( '-g', '--git ', 'git repository' ) do|git| + options[:git] = git + end + + opts.on( '-b', '--branch ', 'git branch' ) do|branch| + options[:branch] = branch + end + + #opts.on( '-r', '--remote ', 'remote server id' ) do|remote| + # options[:remote] = remote + #end + + options[:passwd] = "" + opts.on( '-w', '--passwd ', 'password for managing project' ) do|passwd| + options[:passwd] = passwd + end + + opts.on( '-t', '--ftp ', 'ftp server url: ftp://dibsftp:dibsftp@127.0.0.1:1024' ) do|domain| + options[:fdomain] = domain end opts.on( '-h', '--help', 'display this information' ) do - puts opts + opts.help.split("\n").each {|op| puts op if not op.include? "--CHILD"} + exit + end + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() exit end + + opts.on( '-C', '--CHILD', 'child process' ) do + options[:child] = true + end end cmd = ARGV[0] - - if cmd.eql? "create" or cmd.eql? "remove" or cmd.eql? "start" or - cmd.eql? "stop" or cmd.eql? "add" or - cmd =~ /(help)|(-h)|(--help)/ then + if cmd.eql? "create" or cmd.eql? "remove" or + cmd.eql? "start" or cmd.eql? "upgrade" or + cmd.eql? "stop" or cmd.eql? "add-svr" or + cmd.eql? "add-prj" or cmd.eql? "add-os" or + cmd.eql? "fullbuild" or cmd.eql? "register" or + cmd =~ /(-v)|(--version)/ or + cmd =~ /(help)|(-h)|(--help)/ then if cmd.eql? "help" then ARGV[0] = "-h" @@ -83,10 +219,12 @@ def option_parse options[:cmd] = ARGV[0] else - raise ArgumentError, banner + raise ArgumentError, "Usage: build-svr [OPTS] or build-svr -h" end optparse.parse! + + option_error_check options return options end diff --git a/src/build_server/CommonProject.rb b/src/build_server/CommonProject.rb new file mode 100644 index 0000000..10caaaf --- /dev/null +++ b/src/build_server/CommonProject.rb @@ -0,0 +1,102 @@ +=begin + + CommonProject.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" + +class CommonProject + attr_accessor :name, :type, :passwd, :os_list + + # initialize + def initialize( name, type, server, os_list ) + @name = name + @type = type + @passwd = "" + @os_list = os_list + @server = server + @extra_infos = {} + read_ext_info + end + + + #return passwd + def is_passwd_set?() + return ( not @passwd.empty? ) + end + + + def passwd_match?(word) + if not is_passwd_set? then return true end + + if word.eql? @passwd then + return true + else + return false + end + end + + + def write_ext_info + # write to file + info_file = "#{@server.path}/projects/#{@name}/extra" + File.open( info_file, "w" ) do |f| + @extra_infos.each { |key,value| + f.puts "#{key} : #{value}" + } + end + end + + + # set extra info + def read_ext_info + info_file = "#{@server.path}/projects/#{@name}/extra" + if not File.exists? info_file then return end + File.open( info_file, "r" ) do |f| + while (not f.gets and line = f.gets.split(":")) + if not line[1].nil? then + @extra_infos[line[0].strip] = line[1].strip + end + end + end + end + + + def set_log_cnt( cnt ) + @extra_infos["Latest_log_count"] = cnt.to_s + end + + + def get_latest_log_cnt + result = @extra_infos["Latest_log_count"] + if not result.nil? then + return result.to_i + end + return nil + end +end diff --git a/src/build_server/FullBuildJob.rb b/src/build_server/FullBuildJob.rb new file mode 100644 index 0000000..5c83fee --- /dev/null +++ b/src/build_server/FullBuildJob.rb @@ -0,0 +1,255 @@ +=begin + + FullBuildJob.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/builder" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "client.rb" +require "PackageManifest.rb" +require "Version.rb" +require "Builder.rb" +require "RemoteBuilder.rb" +require "BuildServer.rb" +require "JobLog.rb" +require "mail.rb" + +class FullBuildJob + + attr_accessor :id, :server, :pre_jobs, :os, :type + attr_accessor :status, :log, :source_path + attr_accessor :pkgsvr_client, :thread + attr_accessor :is_fullbuild_job + + # initialize + def initialize (server) + @server = server + @id = server.jobmgr.get_new_job_id() + @log = nil + @type = "FULLBUILD" + + @status = "JUST_CREATED" + @host_os = Utils::HOST_OS + @pkgserver_url = @server.pkgserver_url + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + @pre_jobs = [] #pre-requisite jobs + + @is_fullbuild_job = false + end + + + # execute + def execute(sync=false) + @log.info( "Invoking a thread for FULL-BUILD Job #{@id}", Log::LV_USER) + if @status == "ERROR" then return end + @thread = Thread.new { + begin + thread_main() + terminate() + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + } + + if sync then + @thread.join + end + + return true + end + + + # + def init + # mkdir + if not File.exist? @job_root then + FileUtils.mkdir_p @job_root + end + + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) + end + + @log.info( "Initializing job...", Log::LV_USER) + + # create dummy source path + if not File.exist? @source_path then + FileUtils.mkdir_p @source_path + end + + # set up pkgsvr_client + @pkgsvr_client = Client.new(@pkgserver_url, @job_working_dir, @log) + @pkgsvr_client.update + + return true + end + + + #terminate + def terminate() + # report error + if @status == "ERROR" then + @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + else + # clean up + @server.cleaner.clean(@id) + end + + # close logger + @log.close + end + + + #cancel + def cancel() + #TODO + end + + + # check building is possible + def can_be_built_on?(host_os) + return true + end + + + def has_build_dependency?(other_job) + return true + end + + + def has_same_packages?( wjob ) + return self.eql? wjob + end + + + def does_depend_on?( wjob ) + return true + end + + + def does_depended_by?( wjob ) + return true + end + + + def is_connected? + return true + end + + + # return the job is asyncronous job + def is_asynchronous_job? + return false + end + + # set logger + def set_logger( logger ) + @log = logger + end + + + # + # PROTECTED METHODS + # + protected + + + # main module + def thread_main + @log.info( "New Job #{@id} is started", Log::LV_USER) + + # check passwd + + # create sub jobs + build_jobs = [] + @server.prjmgr.projects.each do |prj| + if prj.type != "GIT" then next end + build_jobs += @server.prjmgr.create_new_jobs_for_all_os( prj.name ) + end + + # set full build job flag + build_jobs.each do |job| + job.is_fullbuild_job = true + job.set_parent_job( self ) + end + + # add all jobs to jobmanager + job_status_map = {} # for tracking job status changes + build_jobs.each do |job| + @server.jobmgr.add_internal_job( job ) + + @log.info( "Added new job \"#{job.id}\"(#{job.get_project().name}) for #{job.os}!", + Log::LV_USER) + + if not @server.job_log_url.empty? then + @log.info( " * Log URL : #{@server.job_log_url}/#{job.id}/log", Log::LV_USER) + end + + # set satus + job_status_map[job.id] = job.status + end + + # show job status changes + all_jobs_finished = false + error_exist = false + while not all_jobs_finished + all_jobs_finished = true + build_jobs.each do |job| + + # check status chanaged, if then print + if job_status_map[ job.id ] != job.status then + @log.info("Job #{job.id}(#{job.get_project().name},#{job.os}) is #{job.status}", Log::LV_USER) + job_status_map[ job.id ] = job.status + end + if job.status != "ERROR" and job.status != "FINISHED" then + all_jobs_finished = false + end + if job.status == "ERROR" then error_exist = true end + end + sleep 1 + end + + # check error + if error_exist then + @status = "ERROR" + return + end + + # INFO. don't change this string + @log.info( "Job is completed!", Log::LV_USER) + @status = "FINISHED" + end + +end diff --git a/src/build_server/GitBuildJob.rb b/src/build_server/GitBuildJob.rb index 75fc502..5b1b696 100644 --- a/src/build_server/GitBuildJob.rb +++ b/src/build_server/GitBuildJob.rb @@ -27,194 +27,159 @@ Contributors: =end require "fileutils" +require "thread" $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "BuildJob.rb" require "utils.rb" + class GitBuildJob < BuildJob - attr_accessor :id, :status, :pkginfo, :pkgsvr_client, :thread, :log, :rev_fail_list, :rev_success_list, :source_path # initialize - def initialize ( repos, commit, os, pkgsvr_url, options, server, parent, outstream, resolve) - super() - @rev_fail_list = [] - @rev_success_list = [] - @id = server.jobmgr.get_new_job_id() - @server = server - @parent = parent - @git_repos = repos - @git_commit = commit - @os = os - @host_os = Utils::HOST_OS - if not pkgsvr_url.nil? and not pkgsvr_url.empty? then - @pkgserver_url = pkgsvr_url - else - local_pkgsvr = @server.local_pkgsvr - @pkgserver_url = local_pkgsvr.location + "/" + local_pkgsvr.get_default_dist_name - end - @options = options - @resolve = resolve - @outstream = outstream - - @status = "JUST_CREATED" - @sub_jobs = [] - @job_root = "#{@server.path}/jobs/#{@id}" - @source_path = @job_root+"/temp" - @pkginfo = nil - @job_working_dir=@job_root+"/works" - - @thread = nil - - # mkdir - FileUtils.rm_rf "#{@server.path}/jobs/#{@id}" - FileUtils.mkdir_p "#{@server.path}/jobs/#{@id}" - - # create logger - @log = JobLog.new(self,"#{@server.path}/jobs/#{@id}/log", outstream ) + def initialize( project, os, server ) + super(server.jobmgr.get_new_job_id(), project, os, server) + @git_repos = project.repository + @git_branch = project.branch + @git_commit = nil end def terminate() - # report error if @status == "ERROR" then @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + elsif @status == "CANCELED" then + @log.error( "Job is CANCELED" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) else - # if succeeded, clean up - FileUtils.rm_rf "#{@source_path}" - FileUtils.rm_rf "#{@job_working_dir}" + @log.info( "Job is FINISHED successfully!" , Log::LV_USER) + + # if succeeded, register source info and copy pkginfo.manifest + @log.info( "Updating the source info for project \"#{@project.name}\"" , Log::LV_USER) + @project.add_source_info( @pkginfo.get_version(), @git_commit) + @project.copy_package_info( @pkginfo.get_version(), + "#{@source_path}/package/pkginfo.manifest") + @project.set_log_cnt( @log.cnt ) + @project.write_ext_info + + # clean up + @server.cleaner.clean(@id) + end + + # clean up builder directory if exist? + if Builder.exist? "JB#{@id}" then + Builder.remove("JB#{@id}") end # send mail if ( @server.send_mail.eql? "YES" ) and ( not @pkginfo.nil? ) and ( not @pkginfo.packages.nil? ) then mail_list = [] contents = [] - done_pkg_list = [] contents.push " " contents.push " Git information : #{@git_commit} " contents.push " " contents.push "%-30s| %10s | %10s" % ["package name", "version", "os"] contents.push "---------------------------------------------------------------" - for pkg in @pkginfo.packages + @pkginfo.packages.each do |pkg| if not pkg.os.eql? @os then next end mail_list = mail_list | Mail.parse_email( pkg.maintainer ) contents.push("%-30s| %10s | %10s" % [ pkg.package_name, pkg.version, pkg.os] ) end - - if @status == "ERROR" then - subject = "[DIBS] Build fail" + + if @status == "ERROR" then + subject = "[DIBS] Build fail" contents.push " " contents.push "check log file" contents.push "* Log : #{@server.job_log_url}/#{@id}/log" + elsif @status == "CANCELED" then + subject = "[DIBS] Build canceled" else - subject = "[DIBS] Build success" + subject = "[DIBS] Build success" end Mail.send_mail(mail_list, subject, contents.join("\n")) - end + end # close logger @log.close - - # send END signal , if connectionn is valid - if @status != "ERROR" and not @outstream.nil? then - BuildCommServer.send_end(@outstream) - end - - # close outstream - if not @outstream.nil? then - BuildCommServer.disconnect( @outstream ) - end end # verify - def pre_verify - @log.info( "Verifying job input...", Log::LV_USER) + def init + # mkdir job root + if not File.exist? @job_root then FileUtils.mkdir_p @job_root end - # git clone - if not git_cmd("clone #{@server.git_server_url}#{@git_repos} temp", @job_root) then - @log.error( "Failed on \"git clone #{@server.git_server_url}/#{@git_repos}\"", Log::LV_USER) - @status = "ERROR" - return false + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) end - # git reset - if not git_cmd("reset --hard #{@git_commit}", @source_path) then - @log.error( "Failed on \"git reset --hard #{@git_commit}\"", Log::LV_USER) - @status = "ERROR" - return false - end + @log.info( "Initializing job...", Log::LV_USER) - # check branch name if ALLOWED_GIT_BRANCH is not empty - if not @server.allowed_git_branch.empty? then - is_correct_branch = false - - # origin/{branch_name} - if @git_commit == "origin/#{@server.allowed_git_branch}" then - is_correct_branch = true - else - # get commit id - commit_id = "" - result_line = git_cmd_return("log -1",@source_path) - if result_line != nil then - result_line.each do |l| - if l.start_with?("commit ") then - commit_id = l.split(" ")[1].strip - end - end - end - - # check branch - if not commit_id.empty? and commit_id.length == 40 then - result_line = git_cmd_return("branch --contains=#{commit_id} -r", @source_path) - result_line.each do |l| - if l.include? "origin/#{@server.allowed_git_branch}" then - is_correct_branch = true - end - end - end - end + # if internal job, copy external_pkgs + if @is_internal_job then + @log.info( "Copying external dependent pkgs...", Log::LV_USER) + ext_pkgs_dir = "#{@job_root}/external_pkgs" - if not is_correct_branch then - @log.error( "Wrong branch is used! Check your commit-id again", Log::LV_USER) - @status = "ERROR" - return false + incoming_dir = "#{@server.transport_path}/#{@dock_num}" + if File.exist? incoming_dir then + FileUtils.mv "#{incoming_dir}", "#{ext_pkgs_dir}" end + + FileUtils.mkdir_p incoming_dir + end + + # download source code + @git_commit = @project.get_source_code(@git_repos, @git_branch, @git_commit, @source_path, @log) + if @git_commit.nil? then + @status = "ERROR" + return false end - # check pkginfo.manifest if not File.exist? "#{@source_path}/package/pkginfo.manifest" - @log.error( "package/pkginfo.manifest doest not exist", Log::LV_USER) + @log.error( "package/pkginfo.manifest does not exist", Log::LV_USER) @status = "ERROR" return false end # set up pkg info + begin @pkginfo = PackageManifest.new("#{@source_path}/package/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end # set up pkgsvr_client @pkgsvr_client = Client.new(@pkgserver_url, @job_working_dir, @log) - @pkgsvr_client.update - return true - end + # checking version if not reverse-build job or not internal-job + if not @is_rev_build_check_job and not @is_internal_job then + # check availabiltiy + if not @server.check_job_availability( self ) then + @log.error( "No servers that are able to build your packages.", Log::LV_USER) + @log.error( "Host-OS (#{@os}) is not supported in build server.", Log::LV_USER) + @status = "ERROR" + @server.log.info "Adding the job \"#{@id}\" is canceled" + return false + end + if not check_package_version(@git_commit) then + @status = "ERROR" + return false + end + end - def git_cmd(cmd, working_dir) - build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" - ret = Utils.execute_shell_with_log(build_command,@log) - - return ret + return true end - def git_cmd_return(cmd, working_dir) - build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" - ret = Utils.execute_shell_return(build_command) - - return ret + def set_git_commit( commit ) + @git_commit = commit end + end diff --git a/src/build_server/GitBuildProject.rb b/src/build_server/GitBuildProject.rb new file mode 100644 index 0000000..d06af21 --- /dev/null +++ b/src/build_server/GitBuildProject.rb @@ -0,0 +1,264 @@ +=begin + + GitProject.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +require "CommonProject.rb" +require "GitBuildJob.rb" +require "Version.rb" +require "PackageManifest.rb" + +# mutax for git operation +$git_mutex = Mutex.new + + +class GitBuildProject < CommonProject + attr_accessor :repository, :branch + + # initialize + def initialize( name, repos, branch, server, os_list ) + super(name, "GIT", server, os_list) + @repository = repos + @branch = branch + @source_infos = {} + @package_infos = {} + end + + + # get commid-id of specified package version + def get_commit_id( version ) + return get_source_info( version ) + end + + + # create new job + # if this project cannot support os, return nil + def create_new_job( os ) + if @os_list.include? os then + return GitBuildJob.new( self, os, @server ) + else + return nil + end + end + + + # create new job + def create_new_job_from_version( os, version=nil ) + new_job=create_new_job( os ) + + # set commit id + if version.nil? then + version = get_latest_version() + end + + commit = get_commit_id( version ) + if not commit.nil? then + new_job.set_git_commit( commit ) + end + + return new_job + end + + # get latest package version + def get_latest_version() + versions = @package_infos.keys + if not versions.empty? then + versions.sort! {|x,y| Version.new(x).compare(Version.new(y)) } + return versions[-1] + else + return nil + end + end + + + # get all package version + def get_all_versions() + return @package_infos.keys + end + + + # add source source info + def add_source_info( version, info ) + @source_infos[version] = info + + # write to file + sources_file = "#{@server.path}/projects/#{@name}/sources" + File.open( sources_file, "w" ) do |f| + @source_infos.each { |key,value| + f.puts "#{key},#{value}" + } + end + end + + + # get source info + def get_source_info( version ) + return @source_infos[version] + end + + + # add package info + def add_package_info( version, path ) + begin + pkginfo =PackageManifest.new(path) + rescue => e + puts e.message + return + end + @package_infos[version] = pkginfo + end + + + # get package info + def get_package_info( version ) + return @package_infos[version] + end + + + # copy package info + def copy_package_info(version, file_path) + # check pkginfo directory + pkginfo_dir = "#{@server.path}/projects/#{@name}/pkginfos" + if not File.exist? pkginfo_dir then + FileUtils.mkdir_p pkginfo_dir + end + + # copy + pkginfo_file = "#{pkginfo_dir}/#{version}.manifest" + FileUtils.cp(file_path, pkginfo_file) + + add_package_info(version, pkginfo_file) + end + + + def include_package?(name, version=nil, os=nil) + # check version first + if not version.nil? then + version = get_latest_version() + end + + if version.nil? or @package_infos[version].nil? then return false end + + # check supported os + if not os.nil? and not @os_list.include? os then return false end + + # check name and version + pkginfo=@package_infos[version] + pkg_list = os.nil? ? pkginfo.packages : pkginfo.get_target_packages(os) + pkg_list.each do |pkg| + if pkg.package_name.eql? name then return true end + end + + return false + end + + + # download source code to "source_path" and return its commit-id + def get_source_code( git_repos, git_branch, git_commit, source_path, log ) + $git_mutex.synchronize { + # check git directory + git_path = "#{@server.path}/projects/#{@name}/cache/git" + cache_path = "#{@server.path}/projects/#{@name}/cache" + if not File.exist? cache_path then + FileUtils.mkdir_p cache_path + end + + # check branch name + if File.exist? git_path then + current_branch = git_cmd_return( "branch", git_path).select{|x| x.start_with?("*")}[0].split(" ")[1].strip + if current_branch != git_branch then + log.warn( "Branch name is changed.", Log::LV_USER) + FileUtils.rm_rf git_path + end + end + + # git pull operation + if File.exist? git_path and not git_cmd("pull", git_path,log) then + log.warn( "Failed on \"git pull\"", Log::LV_USER) + FileUtils.rm_rf git_path + end + + # if no git, clone it + if not File.exist? git_path then + # if "git pull" failed, try to "git clone" + if not git_cmd("clone #{git_repos} git", cache_path, log) then + log.error( "Failed on \"git clone #{git_repos}\"", Log::LV_USER) + return nil + end + # git checkout + if not git_cmd("checkout #{git_branch}", git_path, log) then + log.error( "Failed on \"git checkout #{git_branch}\"", Log::LV_USER) + return nil + end + end + + if git_commit.nil? then + # get git commit-id + commit_id = "" + result_line = git_cmd_return("log -1", git_path) + if result_line != nil then + result_line.each do |l| + if l.start_with?("commit ") then + commit_id = l.split(" ")[1].strip + end + end + end + + git_commit = commit_id + else + # git reset + if not git_cmd("reset --hard #{git_commit}", git_path, log) then + log.error( "Failed on \"git reset --hard #{git_commit}\"", Log::LV_USER) + return nil + end + end + + # copy to source path + FileUtils.cp_r(git_path, source_path) + } + + return git_commit + end + + + def git_cmd(cmd, working_dir, log) + build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" + ret = Utils.execute_shell_with_log(build_command,log) + + return ret + end + + + def git_cmd_return(cmd, working_dir) + build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" + ret = Utils.execute_shell_return(build_command) + + return ret + end +end diff --git a/src/build_server/JobClean.rb b/src/build_server/JobClean.rb new file mode 100644 index 0000000..64265d7 --- /dev/null +++ b/src/build_server/JobClean.rb @@ -0,0 +1,192 @@ +=begin + + JobClean.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "BuildServer.rb" +require "Action.rb" +require "ScheduledActionHandler.rb" + +$access_listfile = Mutex.new + +class JobCleanAction < Action + + def initialize( time, job_path, list_file, server ) + super(time,0) + + @job_path = job_path + @job_id = @job_path.split("/")[-1] + @list_file = list_file + @server = server + end + + + def init + $access_listfile.synchronize { + File.open(@list_file, "a") do |f| + f.puts "#{@job_id},#{time.year},#{time.month},#{time.day},#{time.hour},#{time.min},#{time.sec}" + end + } + end + + + def execute + # Start to clean job + @server.log.info "Executing clean action for the job #{@job_id}" + begin + execute_internal() + rescue => e + @server.log.error e.message + @server.log.error e.backtrace.inspect + end + end + + + private + def execute_internal() + # remove directories + if File.exist? "#{@job_path}/buildroot" then + FileUtils.rm_rf "#{@job_path}/buildroot" + end + if File.exist? "#{@job_path}/temp" then + FileUtils.rm_rf "#{@job_path}/temp" + end + if File.exist? "#{@job_path}/external_pkgs" then + FileUtils.rm_rf "#{@job_path}/external_pkgs" + end + + # remove line for the job + $access_listfile.synchronize { + lines = [] + # get all lines + if File.exist? @list_file then + File.open(@list_file,"r") do |f| + f.each_line do |l| + lines.push l + end + end + end + + # write the line except my job_id + File.open(@list_file,"w") do |f| + lines.each do |l| + if l.split(",")[0].eql? @job_id then next end + f.puts l + end + end + } + end +end + + +class JobCleaner + attr_accessor :quit + + # init + def initialize( server ) + @server = server + @handler = ScheduledActionHandler.new + @list_file = "#{BuildServer::CONFIG_ROOT}/#{@server.id}/clean" + end + + + # start thread + def start() + + list_file2 = "#{BuildServer::CONFIG_ROOT}/#{@server.id}/clean_backup" + jobs_path = "#{@server.path}/jobs" + if not File.exist? jobs_path then + FileUtils.mkdir_p jobs_path + end + + # read clean list + clean_list = [] + if File.exist? @list_file then + FileUtils.mv(@list_file,list_file2) + File.open(list_file2, "r") do |f| + f.each_line do |l| + id = l.split(",")[0] + year = l.split(",")[1] + month = l.split(",")[2] + day = l.split(",")[3] + hour = l.split(",")[4] + min = l.split(",")[5] + sec = l.split(",")[6] + + # create job and register + job_path = "#{jobs_path}/#{id}" + time = Time.mktime(year.to_i, month.to_i, day.to_i, hour.to_i, min.to_i, sec.to_i) + @server.log.info "Registered clean-action for the job in list : #{id}" + @handler.register(JobCleanAction.new(time,job_path,@list_file, @server)) + + # add clean list + clean_list.push id + end + end + end + + + # scan all jobs + Dir.new(jobs_path).entries.each do |id| + # skip . or .. + if id.eql? "." or id.eql? ".." then next end + + if not clean_list.include? id then + job_path = "#{jobs_path}/#{id}" + time = Time.now + @server.log.info "Registered clean-action for old job : #{id}" + @handler.register(JobCleanAction.new(time,job_path,@list_file, @server)) + end + end + + # start handler + @handler.start + end + + + # clean after some time + def clean_afterwards(job_id) + time = Time.now + @server.keep_time + job_path = "#{@server.path}/jobs/#{job_id}" + @handler.register(JobCleanAction.new(time, job_path, @list_file, @server)) + + @server.log.info "Registered delayed clean-action for the job #{job_id}" + end + + + # clean directly + def clean(job_id) + time = Time.now + job_path = "#{@server.path}/jobs/#{job_id}" + @handler.register(JobCleanAction.new(time, job_path, @list_file, @server)) + + @server.log.info "Registered clean-action for the job #{job_id}" + end +end diff --git a/src/build_server/JobLog.rb b/src/build_server/JobLog.rb index c46db48..e005207 100644 --- a/src/build_server/JobLog.rb +++ b/src/build_server/JobLog.rb @@ -34,13 +34,67 @@ require "BuildComm.rb" class JobLog < Log - def initialize(job, path, stream_out) - super(path) + def initialize(job, stream_out) + if job.nil? then + super(nil) + else + if not File.exist? "#{job.server.path}/jobs/#{job.id}" then + FileUtils.mkdir_p "#{job.server.path}/jobs/#{job.id}" + end + super("#{job.server.path}/jobs/#{job.id}/log") + end @parent_job=job @second_out = stream_out end + def set_second_out( out ) + @second_out = out + end + + + def init + # comm-begin + if not @second_out.nil? and not @second_out.closed? then + BuildCommServer.send_begin(@second_out) + end + end + + + def close + # close communication + if not @second_out.nil? then + begin + if not @second_out.closed? then + BuildCommServer.send_end(@second_out) + end + rescue + end + BuildCommServer.disconnect(@second_out) + end + + @second_out = nil + end + + + def is_connected? + if @second_out.nil? or @second_out.closed? then + return false + else + return true + end + end + + + def has_second_out? + if @second_out.nil? then + return false + else + return true + end + end + + protected # overide @@ -50,21 +104,17 @@ class JobLog < Log BuildCommServer.send( @second_out, msg ) end rescue - @parent_job.status="ERROR" - close() - error "Connection closed by remote client" + # close second_out + @second_out.close + @second_out = nil - # terminate job - @parent_job.terminate + error "Connection closed by remote client" - # exit thread if independent worker thread - if @parent_job.thread == Thread.current then - error "Thread wiil be terminated" - @parent_job.thread=nil - Thread.exit + # cancel parent job + if not @parent_job.nil? and @parent_job.cancel_state == "NONE" then + @parent_job.cancel_state = "INIT" end end end - end diff --git a/src/build_server/JobManager.rb b/src/build_server/JobManager.rb index 853c74c..e876dc1 100644 --- a/src/build_server/JobManager.rb +++ b/src/build_server/JobManager.rb @@ -27,46 +27,73 @@ Contributors: =end require 'fileutils' +require 'thread' $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" require "SocketJobRequestListener.rb" require "RemoteBuildJob.rb" -require "LocalBuildJob.rb" +require "RegisterPackageJob.rb" require "packageServer.rb" + class JobManager - attr_accessor :max_working_jobs, :jobs + attr_accessor :max_working_jobs, :jobs, :internal_jobs, :reverse_build_jobs + attr_accessor :internal_job_schedule # initialize def initialize( parent ) @parent = parent @jobs = [] + @internal_jobs = [] + @reverse_build_jobs = [] @max_working_jobs=2 @new_job_index = 0 + @internal_job_schedule = Mutex.new + @latest_job_touch = Mutex.new + end + + + # initialize + def init() + # load latest job idx if exist + file_path = "#{BuildServer::CONFIG_ROOT}/#{@parent.id}/latest_job" + if File.exist? file_path then + latest_idx = -1 + File.open( file_path, "r" ) { |f| + f.each_line { |l| + latest_idx = l.strip.to_i + break + } + } + if latest_idx < 0 then latest_idx = -1 end + @new_job_index = latest_idx + 1 + else + @new_job_index = 0 + end end # get new id def get_new_job_id - # check file - server_dir = "#{BuildServer::CONFIG_ROOT}/#{@parent.id}" - if File.exist? "#{server_dir}/latest_job" then - f = File.open( "#{server_dir}/latest_job", "r" ) - @new_job_index = f.gets.strip.to_i + 1 - f.close - end - - # get new id - new_id = @new_job_index - - # save it - f = File.open( "#{server_dir}/latest_job", "w" ) - f.puts "#{new_id}" - f.close - - return new_id + new_idx = 0 + @latest_job_touch.synchronize { + new_idx = @new_job_index + + file_path = "#{BuildServer::CONFIG_ROOT}/#{@parent.id}/latest_job" + File.open( file_path, "w" ) { |f| + f.puts "#{@new_job_index}" + } + + @new_job_index += 1 + } + + return new_idx end + + def create_new_register_job( file_path ) + return RegisterPackageJob.new( file_path, nil, @parent ) + end # add a normal job def add_job ( new_job ) @@ -75,31 +102,51 @@ class JobManager @jobs.push( new_job ) end + # add internal job for multi-build job + def add_internal_job( new_job ) + @parent.log.info "Added new job \"#{new_job.id}\"" + @internal_jobs.push( new_job ) + end + + # add reverse build chek job + def add_reverse_build_job( new_job ) + @parent.log.info "Added new job \"#{new_job.id}\"" + @reverse_build_jobs.push( new_job ) + end + + # stop internal job selection + def stop_internal_job_schedule() + @internal_job_schedule.lock + end + + + # start internal job selection + def resume_internal_job_schedule() + @internal_job_schedule.unlock + end # intialize normal job def initialize_job ( job ) job.status = "INITIALIZING" Thread.new { - # pre-verifiy - if not job.pre_verify or job.status == "ERROR" then - job.status = "ERROR" - @parent.log.info "Adding the job \"#{job.id}\" is canceled" - job.terminate() - Thread.current.exit - end - - # check availabiltiy - if not @parent.check_job_availability( job ) then - job.log.error( "No servers that are able to build your packages.", Log::LV_USER) - job.status = "ERROR" - @parent.log.info "Adding the job \"#{job.id}\" is canceled" - job.terminate() - Thread.current.exit + begin + # init + if not job.init or job.status == "ERROR" then + if job.cancel_state == "NONE" then job.status = "ERROR" end + @parent.log.info "Adding the job \"#{job.id}\" is canceled" + job.terminate() + Thread.current.exit + end + if job.status != "FINISHED" then + job.status = "WAITING" + end + @parent.log.info "Checking the job \"#{job.id}\" was finished!" + rescue => e + @parent.log.error e.message + @parent.log.error e.backtrace.inspect end - - job.status = "WAITING" - @parent.log.info "Checking the job \"#{job.id}\" was finished!" } + @parent.log.info "Job \"#{job.id}\" entered INITIALIZING status" end @@ -117,7 +164,8 @@ class JobManager def execute_remote(job, rserver) # start build - if job.execute_remote( rserver) then + job.set_remote_job(rserver) + if job.execute() then # status change & job control job.status = "REMOTE_WORKING" @parent.log.info "Moved the job \"#{job.id}\" to remote job list" @@ -126,35 +174,119 @@ class JobManager end end + def cancel_job( job) + job.cancel_state = "WORKING" + @parent.log.info "Creating thread for canceling the job \"#{job.id}\"" + Thread.new { + begin + #terminate job thread + if not job.thread.nil? then + job.thread.terminate + job.thread = nil + end + + # job cacncel + job.cancel + + # cancel finished + job.status = "CANCELED" + + # call terminate process for job + job.terminate + rescue => e + @parent.log.error e.message + @parent.log.error e.backtrace.inspect + end + } + end # handle def handle() + # for cancel jobs + (@jobs + @internal_jobs + @reverse_build_jobs).select {|j| j.cancel_state == "INIT" }.each do |job| + cancel_job( job ) + end + + # for reverse build jobs + job_list = @reverse_build_jobs + job_list.each do |job| + # if "ERROR", "FINISHED", "CANCELED" remove it from list + if job.status == "ERROR" + @parent.log.info "Job \"#{job.id}\" is stopped by ERROR" + @reverse_build_jobs.delete job + elsif job.status == "FINISHED" + @parent.log.info "Job \"#{job.id}\" is removed by FINISH status" + @reverse_build_jobs.delete job + elsif job.status == "CANCELED" + @parent.log.info "Job \"#{job.id}\" is removed by CANCELED status" + @reverse_build_jobs.delete job + end + + # if "JUST_CREATED", initialize it + if job.status == "JUST_CREATED" then + initialize_job( job ) + end + end + + # for internal jobs + job_list = @internal_jobs + job_list.each do |job| + # if "ERROR", "FINISHED", "CANCELED" remove it from list + if job.status == "ERROR" + @parent.log.info "Job \"#{job.id}\" is stopped by ERROR" + @internal_jobs.delete job + elsif job.status == "FINISHED" + @parent.log.info "Job \"#{job.id}\" is removed by FINISH status" + @internal_jobs.delete job + elsif job.status == "CANCELED" + @parent.log.info "Job \"#{job.id}\" is removed by CANCELED status" + @internal_jobs.delete job + end - # if "ERROR", "FINISHED", remove it from list - for job in @jobs + # if "JUST_CREATED", initialize it + if job.status == "JUST_CREATED" then + initialize_job( job ) + end + end + + # for normal job + job_list = @jobs + job_list.each do |job| + # if "ERROR", "FINISHED", "CANCELED" remove it from list if job.status == "ERROR" @parent.log.info "Job \"#{job.id}\" is stopped by ERROR" @jobs.delete job elsif job.status == "FINISHED" + @parent.log.info "Job \"#{job.id}\" is removed by FINISH status" + @jobs.delete job + elsif job.status == "CANCELED" + @parent.log.info "Job \"#{job.id}\" is removed by CANCELED status" @jobs.delete job end - end - # if "JUST_CREATED", initialize it - for job in @jobs - if job.status != "JUST_CREATED" then next end - initialize_job( job ) + # if "JUST_CREATED", initialize it + if job.status == "JUST_CREATED" then + initialize_job( job ) + end + + # check the connection if job is not asynchronous job + if ( job.status == "WAITING" or job.status == "REMOTE_WORKING" or job.status == "PENDING") and + not job.is_asynchronous_job? and + not job.is_connected? then + + job.status = "ERROR" + @jobs.delete( job ) + @parent.log.info "Job \"#{job.id}\" is disconnected by user. Removed!" + end end - # get available job + # reverse build job -> internal job -> normal job job = get_available_job # available job not exist?, continue if not job.nil? then # oherwise, check remote server rserver = @parent.get_available_server( job ) - - # request for build if rserver != nil and rserver == @parent then execute(job) elsif rserver != nil then @@ -164,92 +296,70 @@ class JobManager end end - # check the connection if job is not asynchronous job - for job in @jobs - if ( job.status == "WAITING" or job.status == "REMOTE_WORKING") and - not job.is_asynchronous_job? and - not job.is_connected? then - - @jobs.delete( job ) - @parent.log.info "Job \"#{job.id}\" is disconnected by user. Removed!" - end - end end - # select the job whith no build-dependency problem + # select the job whith no build-dependency problem def get_available_job - - # gather all working jobs - all_working_jobs = [] - for job in @jobs - if job.status == "WORKING" or job.status == "REMOTE_WORKING" then - all_working_jobs.push job + # select reverse build job with round-robin method + selected_job = nil + @reverse_build_jobs.each do |job| + if job.status == "WAITING" then + selected_job = job + break end end - - # for waiting jobs - for job in @jobs - if job.status != "WAITING" then next end - - blocked_by = [] - is_changed = false - - # should not have same packages with workings - # should not depend on workings - # should not be depended by workings - for wjob in all_working_jobs - if job.has_build_dependency?( wjob ) then - - # if there are some changes, check it - blocked_by.push wjob - if not job.blocked_by.include? wjob then is_changed = true end - end + # rotate array + if @reverse_build_jobs.count > 0 then + @reverse_build_jobs.push @reverse_build_jobs.shift + end + if not selected_job.nil? then return selected_job end + + # if no reverse build job exist! + @internal_job_schedule.synchronize { + # internal job first + ret = nil + if @internal_jobs.count > 0 then + ret = get_available_job_in_list(@internal_jobs, true) end - - # if available , then FIFO - if blocked_by.empty? then - job.blocked_by = [] - return job - else - # check count - if blocked_by.count != job.blocked_by.count then is_changed = true end - # if somthing changed, print it and save it - if is_changed then - job.log.info( "Waiting for finishing following jobs:", Log::LV_USER) - for bjob in blocked_by - job.log.info( " * #{bjob.id} #{bjob.pkginfo.packages[0].source}", Log::LV_USER) - end - job.blocked_by = blocked_by - end + # not found, select normal job + if ret.nil? then + ret = get_available_job_in_list(@jobs, false) end - end - - return nil + + return ret + } end # return "max_working_jobs_cnt - current_working_jobs_cnt" def get_number_of_empty_room working_cnt = 0 - for job in @jobs + parent_list = [] + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| if job.status == "WORKING" then working_cnt = working_cnt + 1 end + + # must exclude parent job + if not job.get_parent_job().nil? then + parent_list.push job.get_parent_job() + end end - return @max_working_jobs - working_cnt + parent_list.uniq! + + return @max_working_jobs - working_cnt + parent_list.count end # check there are working jobs def has_working_jobs - working_cnt = 0 - for job in @jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| if job.status == "WORKING" then return true - end + end end return false @@ -258,11 +368,10 @@ class JobManager # check there are waiting jobs def has_waiting_jobs - waiting_cnt = 0 - for job in @jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| if job.status == "WAITING" then return true - end + end end return false @@ -271,44 +380,124 @@ class JobManager def get_working_jobs result = [] - for job in @jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| if job.status == "WORKING" then result.push job end end + return result end def get_waiting_jobs result = [] - for job in @jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| if job.status == "WAITING" then result.push job end end + return result end def get_remote_jobs result = [] - for job in @jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| if job.status == "REMOTE_WORKING" then result.push job end end + return result end def get_pending_jobs result = [] - for job in @jobs + @jobs.each do |job| if job.status == "PENDING" then result.push job end end return result end + + + protected + # select the job whith no build-dependency problem + # if "check_dep_wait" is true, it will check the build dependency + # among items of "WAIT" status in list + def get_available_job_in_list( jobs, check_dep_wait=false ) + + # gather all working jobs and full-build jobs + check_dep_jobs = [] + jobs.each do |job| + if job.cancel_state != "NONE" then next end + + if job.status == "WORKING" or job.status == "REMOTE_WORKING" or job.status == "PENDING" then + check_dep_jobs.push job + elsif ( check_dep_wait and job.status == "WAITING") then + check_dep_jobs.push job + end + end + + # for waiting jobs + jobs.each do |job| + if job.cancel_state != "NONE" then next end + if job.status != "WAITING" then next end + + # check build dependency against working job + pre_jobs = [] + check_dep_jobs.each do |cjob| + if job == cjob then next end + # In case that "WORKING/REMOTE_WORKING" job has build dependency on me + if (cjob.status == "WORKING" or cjob.status == "REMOTE_WORKING" ) and + (job.has_build_dependency?( cjob ) or job.is_compatible_with?( cjob)) then + pre_jobs.push cjob + # In case that "PENDING" job is depends on me (not depended ) + elsif cjob.status == "PENDING" and (not job.does_depend_on? cjob) and + (job.has_build_dependency?( cjob ) or job.is_compatible_with?( cjob)) then + pre_jobs.push cjob + elsif check_dep_wait and cjob.status == "WAITING" and + (job.does_depend_on? cjob or + (job.id > cjob.id and job.is_compatible_with? cjob) ) then + pre_jobs.push cjob + end + end + + # check pre-requisite jobs are changed, notify to user + is_changed = false + if pre_jobs.count != job.pre_jobs.count then + is_changed=true + else + pre_jobs.each do |pjob| + if not job.pre_jobs.include? pjob then + is_changed = true + break + end + end + end + if pre_jobs.count > 0 and is_changed then + job.log.info( "Waiting for finishing following jobs:", Log::LV_USER) + pre_jobs.each do |bjob| + if bjob.type == "BUILD" then + job.log.info( " * #{bjob.id} #{bjob.pkginfo.packages[0].source}", Log::LV_USER) + elsif bjob.type == "MULTIBUILD" then + job.log.info( " * #{bjob.id} (Multi Build Job)", Log::LV_USER) + end + end + end + job.pre_jobs = pre_jobs + + # no pre-requisite jobs, return its job + if job.pre_jobs.count == 0 then + return job + end + end + + return nil + end + end diff --git a/src/build_server/LocalBuildJob.rb b/src/build_server/LocalBuildJob.rb deleted file mode 100644 index e178616..0000000 --- a/src/build_server/LocalBuildJob.rb +++ /dev/null @@ -1,148 +0,0 @@ -=begin - - LocalBuildJob.rb - -Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. - -Contact: -Taejun Ha -Jiil Hyoun -Donghyuk Yang -DongHee Yang - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -Contributors: -- S-Core Co., Ltd -=end - -$LOAD_PATH.unshift File.dirname(__FILE__) -$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" -require "BuildJob.rb" -require "utils.rb" - -class LocalBuildJob < BuildJob - attr_accessor :id, :status, :pkginfo, :pkgsvr_client, :thread, :log, :rev_fail_list, :rev_success_list, :source_path - - # initialize - def initialize (local_path, os, pkgserver_url, options, server, parent, outstream, resolve ) - super() - @rev_fail_list = [] - @rev_success_list = [] - @id = server.jobmgr.get_new_job_id() - @server = server - @parent = parent - @local_path = local_path - @os = os - @host_os = Utils::HOST_OS - if not pkgserver_url.nil? then - @pkgserver_url = pkgsvr_url - else - local_pkgsvr = @server.local_pkgsvr - @pkgserver_url = local_pkgsvr.location + "/" + local_pkgsvr.get_default_dist_name - end - @options = options - @resolve = resolve - @outstream = outstream - - @status = "JUST_CREATED" - @sub_jobs = [] - @job_root = "#{@server.path}/jobs/#{@id}" - @source_path = @local_path - @pkginfo = nil - @pkgsvr_client = nil - @job_working_dir=@job_root+"/works" - - @thread = nil - - # mkdir - FileUtils.rm_rf "#{@server.path}/jobs/#{@id}" - FileUtils.mkdir_p "#{@server.path}/jobs/#{@id}" - - # create logger - @log = JobLog.new( self, "#{@server.path}/jobs/#{@id}/log", outstream ) - end - - - def terminate() - - # report error - if @status == "ERROR" then - @log.error( "Job is stopped by ERROR", Log::LV_USER) - else - # if succeeded, clean up - FileUtils.rm_rf "#{@job_working_dir}" - end - - # send mail - if ( @server.send_mail.eql? "YES" ) and ( not @pkginfo.nil? ) and ( not @pkginfo.packages.nil? ) then - mail_list = [] - contents = [] - done_pkg_list = [] - contents.push " " - contents.push "%-30s| %10s | %10s" % ["package name", "version", "os"] - contents.push "---------------------------------------------------------------" - for pkg in @pkginfo.packages - if not pkg.os.eql? @os then next end - mail_list = mail_list | Mail.parse_email( pkg.maintainer ) - contents.push("%-30s| %10s | %10s" % [ pkg.package_name, pkg.version, pkg.os] ) - end - - if @status == "ERROR" then - subject = "[DIBS] Build fail" - contents.push " " - contents.push "check log file" - contents.push "* Log : #{@server.job_log_url}/#{@id}/log" - else - subject = "[DIBS] Build success" - end - Mail.send_mail(mail_list, subject, contents.join("\n")) - end - - # close logger - @log.close - - # send END signal , if connectionn is valid - if @status != "ERROR" and not @outstream.nil? then - BuildCommServer.send_end(@outstream) - end - - # close outstream - if not @outstream.nil? then - BuildCommServer.disconnect( @outstream ) - end - end - - - # verify - def pre_verify - @log.info( "Verifying job input...", Log::LV_USER) - - # check pkginfo.manifest - if not File.exist? "#{@source_path}/package/pkginfo.manifest" - @log.error( "#{@source_path}/package/pkginfo.manifest doest not exist", Log::LV_USER) - @status = "ERROR" - return false - end - - # set pkginfo - @pkginfo = PackageManifest.new("#{@source_path}/package/pkginfo.manifest") - - # set up pkgsvr_client - @pkgsvr_client = Client.new(@pkgserver_url, @job_working_dir, @log) - @pkgsvr_client.update - - return true - end - -end diff --git a/src/build_server/MultiBuildJob.rb b/src/build_server/MultiBuildJob.rb new file mode 100644 index 0000000..9e28c9e --- /dev/null +++ b/src/build_server/MultiBuildJob.rb @@ -0,0 +1,489 @@ +=begin + + MultiBuildJob.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/builder" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "client.rb" +require "PackageManifest.rb" +require "Version.rb" +require "Builder.rb" +require "RemoteBuilder.rb" +require "BuildServer.rb" +require "JobLog.rb" +require "mail.rb" + +class MultiBuildJob + + attr_accessor :id, :server, :pre_jobs, :os, :type + attr_accessor :status, :log, :source_path, :cancel_state + attr_accessor :pkgsvr_client, :thread, :sub_jobs + + # initialize + def initialize (server) + @server = server + @id = server.jobmgr.get_new_job_id() + @log = nil + @type = "MULTIBUILD" + @os = "Unknown" + + @status = "JUST_CREATED" + @host_os = Utils::HOST_OS + @pkgserver_url = @server.pkgserver_url + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + @pre_jobs = [] #pre-requisite jobs + @cancel_state = "NONE" + + # children + @sub_jobs = [] + end + + + def get_buildroot() + return @buildroot_dir + end + + + def get_parent_job() + return nil + end + + + def is_rev_build_check_job() + return false + end + + # execute + def execute(sync=false) + @log.info( "Invoking a thread for MULTI-BUILD Job #{@id}", Log::LV_USER) + if @status == "ERROR" then return end + @thread = Thread.new { + begin + # main + thread_main() + + # close + terminate() + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + } + + if sync then + @thread.join + end + + return true + end + + # cnacel + def cancel() + @sub_jobs.select{|x| x.cancel_state == "NONE"}.each do |sub| + sub.cancel_state = "INIT" + end + if not @log.nil? then + @log.info( "JOB is canceled by cancel operation !!", Log::LV_USER) + end + end + + # + def init + # mkdir + if not File.exist? @job_root then + FileUtils.mkdir_p @job_root + end + + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) + end + + @log.info( "Initializing job...", Log::LV_USER) + + # create source path + if not File.exist? @source_path then + FileUtils.mkdir_p @source_path + end + + # initialize all sub jobs and add them to "internal_jobs" + @sub_jobs.each do |job| + # initialize job + if not job.init or job.status == "ERROR" then + job.status = "ERROR" + @log.info( "Failed to initialize sub-job \"#{job.get_project().name}\" for #{job.os}. (#{job.id})", Log::LV_USER) + job.terminate() + end + + if job.status != "ERROR" then + job.status = "WAITING" + else + job.status = "ERROR" + @status = "ERROR" + end + end + if @status == "ERROR" then + return false + end + + + # set up pkgsvr_client + @pkgsvr_client = Client.new(@pkgserver_url, @job_working_dir, @log) + + return true + end + + + #terminate + def terminate() + + # report error + if @status == "ERROR" then + # register delayed clean action for sub jobs + @sub_jobs.each do |job| + @server.cleaner.clean_afterwards(job.id) + end + + # register delayed clean action for me + @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + + elsif @status == "CANCELED" then + # register delayed clean action for sub jobs + @sub_jobs.each do |job| + @server.cleaner.clean_afterwards(job.id) + end + + # register delayed clean action for me + @log.error( "Job is stopped by CANCEL" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + + else + # terminate all sub jobs + @sub_jobs.each do |job| + if not job.log.nil? then job.terminate() end + end + + # register direct clean action for me + @server.cleaner.clean(@id) + end + + # close logger + @log.close + end + + + def is_sub_job? + return false + end + + + def get_sub_jobs() + return @sub_jobs + end + + + # check building is possible + def can_be_built_on?(host_os) + return true + end + + + def get_packages() + packages = [] + @sub_jobs.each do |job| + packages = packages + job.get_packages() + end + packages.uniq! + + return packages + end + + + def get_build_dependencies(target_os) + deps = [] + @sub_jobs.each do |job| + deps = deps + job.get_build_dependencies(target_os) + end + deps.uniq! + + return deps + end + + + def get_source_dependencies(target_os, host_os) + deps = [] + @sub_jobs.each do |job| + deps = deps + job.get_source_dependencies(target_os,host_os) + end + deps.uniq! + + return deps + end + + + def is_compatible_with?(o) + return false + end + + def has_build_dependency?(other_job) + + if has_same_packages?(other_job) or + does_depend_on?(other_job) or + does_depended_by?(other_job) then + + return true + else + return false + end + end + + + def has_same_packages?( wjob ) + + # same package must have same os + if not @os.eql? wjob.os then + return false + end + + # check package name + get_packages.each do |pkg| + wjob.get_packages().each do |wpkg| + if pkg.package_name == wpkg.package_name then + #puts "Removed from candiated... A == B" + return true + end + end + end + + return false + end + + + def does_depend_on?( wjob ) + + # compare build dependency + get_build_dependencies(@os).each do |dep| + wjob.get_packages().each do |wpkg| + # dep packages of my job must have same name and target os + # with packages in working job + if dep.package_name == wpkg.package_name and + dep.target_os_list.include? wjob.os then + #puts "Removed from candiated... A -> B" + return true + end + end + end + + return false + end + + + def does_depended_by?( wjob ) + + get_packages().each do |pkg| + wjob.get_build_dependencies(wjob.os).each do |dep| + # dep package of working job must have same name and target os + # with packages in my job + if dep.package_name == pkg.package_name and + dep.target_os_list.include? @os then + #puts "Checking... A <- B" + return true + end + end + end + return false + end + + + def is_connected? + return true + end + + + # return the job is asyncronous job + def is_asynchronous_job? + return false + end + + # set logger + def set_logger( logger ) + @log = logger + end + + + # add sub job + def add_sub_job( job ) + @sub_jobs.push job + # this will make sub-job to share build-root of parent + job.set_parent_job( self ) + end + + + def progress + # do noting + return "" + end + + + def get_log_url() + # only when server support log url + if @server.job_log_url.empty? then + return "","" + end + + return "#{@server.job_log_url}/#{@id}/log","" + end + + # + # PROTECTED METHODS + # + protected + + + # main module + def thread_main + @log.info( "New Job #{@id} is started", Log::LV_USER) + + # initialize status map + job_status_map = {} + @sub_jobs.each do |job| + job_status_map[job.id] = job.status + end + + # add to internal job + @server.jobmgr.internal_job_schedule.synchronize { + @sub_jobs.each do |job| + # init finished, add internal_jobs + @server.jobmgr.add_internal_job(job) + @log.info( "Added new job \"#{job.get_project().name}\" for #{job.os}! (#{job.id})", + Log::LV_USER) + if not @server.job_log_url.empty? then + @log.info( " * Log URL : #{@server.job_log_url}/#{job.id}/log", Log::LV_USER) + end + end + } + + # show job status changes + all_jobs_finished = false + stop_status = "FINISHED" + while not all_jobs_finished + all_jobs_finished = true + @sub_jobs.each do |job| + # check status chanaged, if then print + if job_status_map[ job.id ] != job.status then + @log.info(" * Sub-Job \"#{job.get_project().name}(#{job.os})\" has entered \"#{job.status}\" state. (#{job.id})", Log::LV_USER) + job_status_map[ job.id ] = job.status + end + # check all jobs are finished + if job.status != "ERROR" and job.status != "FINISHED" and job.status != "CANCELED" then + all_jobs_finished = false + end + # check there is some error or cancel + if stop_status == "FINISHED" and + (job.status == "ERROR" or job.status == "CANCELED") then + # write url + write_log_url(job) + # cancel all other un-finished jobs + @sub_jobs.each do |sub| + if sub.status != "ERROR" and sub.status != "FINISHED" and + sub.status != "CANCELED" and sub.cancel_state == "NONE" then + @log.info(" * Sub-Job \"#{sub.get_project().name}(#{sub.os})\" has entered \"CANCELING\" state. (#{sub.id})", Log::LV_USER) + sub.cancel_state = "INIT" + end + end + + stop_status = job.status + break + end + end + + # + sleep 1 + end + + if stop_status == "ERROR" or stop_status == "CANCELED" then + @status = stop_status + return + end + + # upload + if not upload() then + @status = "ERROR" + return + end + + # INFO. don't change this string + @log.info( "Job is completed!", Log::LV_USER) + @status = "FINISHED" + end + + + def upload() + @log.info( "Uploading ...", Log::LV_USER) + + # get package path list + binpkg_path_list = Dir.glob("#{@source_path}/*_*_*.zip") + + # upload + u_client = Client.new( @server.pkgserver_url, nil, @log ) + snapshot = u_client.upload( @server.pkgserver_addr, @server.pkgserver_port, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd, binpkg_path_list) + + if snapshot.nil? then + @log.info( "Upload failed...", Log::LV_USER) + + return false + end + + # update local + @log.info( "Upload succeeded. Sync local pkg-server again...", Log::LV_USER) + @pkgsvr_client.update + @log.info("Snapshot: #{snapshot}", Log::LV_USER) + + return true + end + + + # write web url for log + private + def write_log_url(job) + url,remote_url = job.get_log_url() + if not url.empty? then + @log.info( " ** Log1: #{url}", Log::LV_USER) + end + if not remote_url.empty? then + @log.info( " ** Log2: #{remote_url}", Log::LV_USER) + end + end + +end diff --git a/src/build_server/PackageSync.rb b/src/build_server/PackageSync.rb new file mode 100644 index 0000000..f395856 --- /dev/null +++ b/src/build_server/PackageSync.rb @@ -0,0 +1,187 @@ +=begin + + PackageSync.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "BuildServer.rb" +require "Action.rb" +require "ScheduledActionHandler.rb" + + +class PackageSyncAction < Action + @@new_id = 0 + + def initialize( time, url, proxy, server ) + super(time, server.pkg_sync_period) + my_id = @@new_id + @@new_id += 1 + @pkgsvr_url = url + @proxy = proxy + @server = server + @pkgsvr_client = nil + @main_client = nil + @sync_root = "#{@server.path}/sync/#{my_id}" + @download_path = "#{@sync_root}/remote" + @original_path = "#{@sync_root}/main" + end + + + def init + # create directory + if File.exist? @download_path then + FileUtils.rm_rf @download_path + FileUtils.rm_rf @original_path + else + FileUtils.mkdir_p @download_path + FileUtils.mkdir_p @original_path + end + + # create client + @pkgsvr_client = Client.new( @pkgsvr_url, @download_path, @server.log ) + @main_client = Client.new( @server.pkgserver_url, @original_path, @server.log ) + end + + + def execute + @server.log.info "Executing package-sync action for server \"#{@pkgsvr_url}\"" + + begin + execute_internal() + rescue => e + @server.log.error e.message + @server.log.error e.backtrace.inspect + end + end + + + private + def execute_internal() + # check update + pkgs = check_package_update + + # if updates are found, download them + downloaded_files = [] + pkgs.each { |pkg| + pkg_name=pkg[0]; os=pkg[1] + + files = @pkgsvr_client.download(pkg_name, os, false) + downloaded_files += files + } + + # request to register + registered_jobs = [] + downloaded_files.each { |file_path| + @server.log.info "Creating new job for registering \"#{file_path}\"" + new_job = @server.jobmgr.create_new_register_job( file_path ) + logger = JobLog.new( new_job, nil ) + new_job.set_logger(logger) + logger.init + + # add + @server.jobmgr.add_job( new_job ) + registered_jobs.push new_job + } + + # wait for finish all jobs + all_jobs_finished = false + while not all_jobs_finished + unfinished_jobs = registered_jobs.select { |j| + (j.status != "ERROR" and j.status != "FINISHED" and j.status != "CANCELED") + } + if unfinished_jobs.empty? then + all_jobs_finished = true + else + sleep 10 + end + end + + # remove files + downloaded_files.each { |file_path| + @server.log.info "Removed downloaded file: \"#{file_path}\"" + FileUtils.rm_rf file_path + } + end + + + protected + def check_package_update + pkgs = [] + + # update + @pkgsvr_client.update() + @main_client.update() + + # for all BINARY project + bin_prjs = @server.prjmgr.projects.select { |p| (p.type == "BINARY") } + bin_prjs.each { |p| + pkg_name = p.pkg_name + p.os_list.each { |os| + # get pkg version in server + main_ver = @main_client.get_attr_from_pkg(pkg_name, os, "version") + if main_ver.nil? then next end + remote_ver = @pkgsvr_client.get_attr_from_pkg(pkg_name, os, "version") + if remote_ver.nil? then next end + + if Version.new(main_ver) < Version.new(remote_ver) then + pkgs.push [pkg_name, os] + end + } + } + + return pkgs + end + +end + + +class PackageServerSynchronizer + attr_accessor :quit + + # init + def initialize( server ) + @server = server + @handler = ScheduledActionHandler.new + end + + + # start thread + def start() + + time = Time.new + 60 + @server.remote_pkg_servers.each { |entry| + url=entry[0]; proxy=entry[1] + @handler.register( PackageSyncAction.new(time, url, proxy, @server) ) + @server.log.info "Registered package-sync action for server \"#{url}\"" + } + + # start handler + @handler.start + end +end diff --git a/src/build_server/ProjectManager.rb b/src/build_server/ProjectManager.rb new file mode 100644 index 0000000..9d61df1 --- /dev/null +++ b/src/build_server/ProjectManager.rb @@ -0,0 +1,352 @@ +=begin + + ProjectManager.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +$LOAD_PATH.unshift File.dirname(__FILE__) +require "GitBuildProject.rb" +require "BinaryUploadProject.rb" +require "MultiBuildJob.rb" +require "PackageManifest.rb" +require "package.rb" + +class ProjectManager + attr_accessor :projects + + # initialize + def initialize( server ) + @server = server + @projects = [] + @project_root = "#{@server.path}/projects" + end + + + # load existing project from server configuration + def load() + # check project root + if not File.exist? @project_root then + FileUtils.mkdir_p @project_root + end + + # scan all projects + Dir.new(@project_root).entries.each do |name| + # skip . or .. + if name.eql? "." or name.eql? ".." then next end + + # create project + @server.log.info "Loading project : #{name}" + prj = load_project( name ) + if not prj.nil? then + @projects.push prj + end + end + + end + + + # get_project of the name + def get_project ( name ) + @projects.each do |prj| + if prj.name.eql? name then return prj end + end + + return nil + end + + + def add_git_project(name , repos, branch, passwd, os_list) + prj = get_project( name) + if not prj.nil? then return false end + + new_prj = GitBuildProject.new(name, repos, branch, @server, os_list) + if not passwd.nil? and not passwd.empty? then + new_prj.passwd = passwd + end + @projects.push new_prj + + # check project directory + if not File.exist? "#{@project_root}/#{name}" then + FileUtils.mkdir_p "#{@project_root}/#{name}" + end + + # write configuration + write_configuration(name, repos, branch, passwd, os_list) + + return true + end + + + def add_binary_project(name, pkg_name, passwd, os_list) + prj = get_project( name) + if not prj.nil? then return false end + + new_prj = BinaryUploadProject.new(name, pkg_name, @server, os_list) + if not passwd.nil? and not passwd.empty? then + new_prj.passwd = passwd + end + @projects.push new_prj + + # check project directory + if not File.exist? "#{@project_root}/#{name}" then + FileUtils.mkdir_p "#{@project_root}/#{name}" + end + + # write configuration + write_configuration_for_binary_project(name, pkg_name, passwd, os_list) + + return true + end + + + def add_remote_project( name, server_id) + end + + + # create new job for project + # if cannot create, return nil + def create_new_job( name, os ) + prj = get_project( name ) + if prj.nil? then return nil end + + return prj.create_new_job( os ) + end + + + # create new multi build job + def create_new_multi_build_job( sub_job_list ) + result = MultiBuildJob.new( @server ) + + sub_job_list.each do |job| + result.add_sub_job( job ) + end + + return result + end + + + # create new full job + def create_new_full_build_job( ) + # create multi job + result = MultiBuildJob.new( @server ) + + # create sub jobs + @projects.each do |prj| + if prj.type != "GIT" then next end + + prj.os_list.each do |os| + if not @server.supported_os_list.include? os then next end + + new_job = create_new_job( prj.name, os ) + if new_job.nil? then next end + + # This make project to build + # even though there is a package of same version on pkg-server + new_job.set_force_rebuild(true) + + # add to multi job + result.add_sub_job( new_job ) + end + end + + return result + end + + + # get project that includes specified pkg name and os + # will return [project,os,ver] list + def get_projects_from_pkgs(pkgs) + result = [] + @projects.each do |prj| + pkgs.each do |pkg| + name = pkg.package_name + ver = pkg.version + os = pkg.os + + # check project provide target package + if prj.include_package?(name, ver, os) then + result.push [prj, os, ver] + break + end + end + end + + return result + end + + + def get_project_from_package_name(pkg_name) + @projects.each do |prj| + # check project provide target package + if prj.include_package?(pkg_name) then + return prj + end + end + + return nil + end + + + # get project from git repository + def get_git_project( repos ) + @projects.each { |prj| + if prj.type == "GIT" and prj.repository == repos then + return prj + end + } + + return nil + end + + + def create_unnamed_git_project(repos) + name = "UNNAMED_PRJ_#{@projects.count}" + branch = "master" + passwd = nil + os_list = Utils.get_all_OSs() + # add + add_git_project(name , repos, branch, passwd, os_list) + # get + return get_project(name) + end + + protected + + # load and create project + def load_project(name) + + # check config file + config_file = "#{@project_root}/#{name}/build" + if not File.exist? config_file then return nil end + + # read configuration + type="GIT" + passwd="" + repos="none" + branch="master" + os_list = @server.supported_os_list + rserver_id=nil + pkg_name=nil + File.open( config_file, "r" ) do |f| + f.each_line do |l| + idx = l.index("=") + 1 + length = l.length - idx + + if l.start_with?("TYPE=") + type = l[idx,length].strip + elsif l.start_with?("PASSWD=") + passwd = l[idx,length].strip + elsif l.start_with?("GIT_REPOSITORY=") + repos = l[idx,length].strip + elsif l.start_with?("GIT_BRANCH=") + branch = l[idx,length].strip + elsif l.start_with?("OS_LIST=") + os_list = l[idx,length].strip.split(",") + elsif l.start_with?("REMOTE_SERVER_ID=") + rserver_id = l[idx,length].strip + elsif l.start_with?("PACKAGE_NAME=") + pkg_name = l[idx,length].strip + else + next + end + end + end + + # write back & create project + if type == "GIT" then + write_configuration(name, repos, branch, passwd, os_list) + new_project = GitBuildProject.new(name, repos, branch, @server, os_list) + + # read source info + sources_file = "#{@project_root}/#{name}/sources" + if File.exist? sources_file then + File.open(sources_file, "r") do |f| + f.each_line do |l| + version = l.split(",")[0].strip + info = l.split(",")[1].strip + + new_project.add_source_info( version, info ) + end + end + end + + # read pkginfo + pkginfo_dir = "#{@project_root}/#{name}/pkginfos" + if not File.exist? pkginfo_dir then FileUtils.mkdir_p pkginfo_dir end + Dir.new(pkginfo_dir).entries.each do |file| + if file.eql? "." or file.eql? ".." then next end + + vlen = file.length - ".manifest".length + version = file[0,vlen] + new_project.add_package_info( version, "#{pkginfo_dir}/#{file}" ) + end + + elsif type == "BINARY" then + write_configuration_for_binary_project(name, pkg_name, passwd, os_list) + new_project = BinaryUploadProject.new(name, pkg_name, @server, os_list) + end + + + # set passwd if exist + if not passwd.empty? then + new_project.passwd = passwd + end + + + return new_project + end + + + # write configuration + def write_configuration(name, repos, branch, passwd, os_list ) + config_file = "#{@project_root}/#{name}/build" + File.open( config_file, "w" ) do |f| + f.puts "TYPE=GIT" + if not passwd.nil? and not passwd.empty? then + f.puts "PASSWD=#{passwd}" + end + f.puts "GIT_REPOSITORY=#{repos}" + f.puts "GIT_BRANCH=#{branch}" + f.puts "OS_LIST=#{os_list.join(",")}" + end + end + + + # write configuration + def write_configuration_for_binary_project(name, pkg_name, passwd, os_list ) + config_file = "#{@project_root}/#{name}/build" + File.open( config_file, "w" ) do |f| + f.puts "TYPE=BINARY" + if not passwd.nil? and not passwd.empty? then + f.puts "PASSWD=#{passwd}" + end + f.puts "PACKAGE_NAME=#{pkg_name}" + f.puts "OS_LIST=#{os_list.join(",")}" + end + end + + +end diff --git a/src/build_server/RegisterPackageJob.rb b/src/build_server/RegisterPackageJob.rb new file mode 100644 index 0000000..0655ef0 --- /dev/null +++ b/src/build_server/RegisterPackageJob.rb @@ -0,0 +1,544 @@ +=begin + + RegisterBinaryJob.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/builder" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "client.rb" +require "PackageManifest.rb" +require "Version.rb" +require "BuildServer.rb" +require "JobLog.rb" +require "mail.rb" +require "utils.rb" +require "ReverseBuildChecker.rb" + +class RegisterPackageJob + + attr_accessor :id, :server, :pre_jobs, :os, :type + attr_accessor :status, :log, :source_path + attr_accessor :pkgsvr_client, :thread, :pkg_type + attr_accessor :pkg_name, :pkginfo, :cancel_state + + + # initialize + def initialize( local_path, project, server, ftpurl=nil ) + @server = server + @id = server.jobmgr.get_new_job_id() + @log = nil + @type = "REGISTER" + + @status = "JUST_CREATED" + @host_os = Utils::HOST_OS + @pkgserver_url = @server.pkgserver_url + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + @cancel_state = "NONE" + @pre_jobs = [] + + @local_path=local_path + @file_path = nil + @filename = File.basename(local_path) + if @filename =~ /.*_.*_.*\.zip/ then + @pkg_type = "BINARY" + new_name = @filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + @pkg_name = new_name.split(",")[0] + @pkg_version = new_name.split(",")[1] + @os = new_name.split(",")[2] + else + @pkg_type = "ARCHIVE" + @pkg_name = @filename + end + @pkginfo = nil #This info is valid only for BINARY package + @project = project + end + + + def is_sub_job? + return false + end + + + def get_project() + return @project + end + + + def get_buildroot() + return @buildroot_dir + end + + def get_parent_job() + return nil + end + + + def is_rev_build_check_job() + return false + end + + # execute + def execute(sync=false) + @log.info( "Invoking a thread for REGISTER Job #{@id}", Log::LV_USER) + if @status == "ERROR" then return end + @thread = Thread.new { + begin + thread_main() + terminate() + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + } + + if sync then + @thread.join + end + + return true + end + + + # + def init + # mkdir + if not File.exist? @job_root then + FileUtils.mkdir_p @job_root + end + + if @cancel_state != "NONE" then return false end + + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) + end + + if @cancel_state != "NONE" then return false end + + @log.info( "Initializing job...", Log::LV_USER) + + # create dummy source path + if not File.exist? @source_path then + FileUtils.mkdir_p @source_path + end + + # copy package file to source path + @file_path = "#{@source_path}/#{File.basename(@local_path)}" + if not File.exist? @local_path then + @log.error( "File not found!", Log::LV_USER) + @status = "ERROR" + return false + else + if not @project.nil? then + # if remote upload remove file and its directory + FileUtils.mv(@local_path, @file_path) + FileUtils.rm_rf("#{File.dirname(@local_path)}") + else + FileUtils.cp(@local_path, @file_path) + end + end + + if @cancel_state != "NONE" then return false end + + # set up pkgsvr_client + @pkgsvr_client = Client.new(@pkgserver_url, @job_working_dir, @log) + + if @cancel_state != "NONE" then return false end + + # check if the os is supported by build server + if @pkg_type == "BINARY" and + not @server.supported_os_list.include? @os then + @log.error( "Unsupported OS \"#{@os}\" is used!", Log::LV_USER) + @status = "ERROR" + return false + end + + if @cancel_state != "NONE" then return false end + + # checking version if not reverse-build job + if @pkg_type == "BINARY" then + # extrac pkg file + cmd = "cd \"#{@source_path}\";unzip #{@file_path}" + if not Utils.execute_shell(cmd) then + @log.error( "Extracting package file failed!", Log::LV_USER) + @status = "ERROR" + return false + end + + if @cancel_state != "NONE" then return false end + + # set up pkg info + begin + @pkginfo = PackageManifest.new("#{@source_path}/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + @status = "ERROR" + return false + end + + if @cancel_state != "NONE" then return false end + + if not check_package_version() then + @status = "ERROR" + return false + end + end + + if @cancel_state != "NONE" then return false end + + return true + end + + + #terminate + def terminate() + # report error + if @status == "ERROR" then + @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + else + # clean up + @server.cleaner.clean(@id) + if not @project.nil? then + @project.set_log_cnt( @log.cnt ) + @project.write_ext_info + end + end + + # close logger + @log.close + end + + + #cancel + def cancel() + if not @log.nil? then + @log.info( "JOB is canceled by cancel operation !!", Log::LV_USER) + end + end + + + # check building is possible + def can_be_built_on?(host_os) + return true + end + + + def get_packages() + if @pkg_type == "BINARY" then + return @pkginfo.packages + else + return [] + end + end + + + def get_build_dependencies(target_os) + return [] + end + + + def get_source_dependencies(target_os,host_os) + return [] + end + + + def is_compatible_with?(o) + return false + end + + + def has_build_dependency?(other_job) + if has_same_packages?(other_job) or + does_depended_by?(other_job) then + + return true + else + return false + end + end + + + def has_same_packages?( wjob ) + if @type != wjob.type then return false end + + case @pkg_type + when "BINARY" + if @pkg_name == wjob.pkg_name and + @os == wjob.os then + return true + end + when "ARCHIVE" + if @pkg_name == wjob.pkg_name then return true end + end + + return false + end + + + # binary/archive package should not have build-dependencies + def does_depend_on?( wjob ) + return false + end + + + def does_depended_by?( wjob ) + if @pkg_type == "BINARY" then + wjob.get_build_dependencies(wjob.os).each do |dep| + # dep package of working job must have same name and target os + # with packages in my job + if dep.package_name == @pkg_name and + dep.target_os_list.include? @os then + #puts "Checking... A <- B" + return true + end + end + else + wjob.get_source_dependencies(wjob.os,@host_os).each do |dep| + if dep.package_name == @pkg_name then + return true + end + end + end + + return false + end + + + def is_connected? + return true + end + + + # return the job is asyncronous job + def is_asynchronous_job? + return false + end + + # set logger + def set_logger( logger ) + @log = logger + end + + + def progress + if not @log.nil? then + if @project.nil? or @project.get_latest_log_cnt.nil? then + return "--% (#{log.cnt.to_s} lines) " + else + return ( ( @log.cnt * 100 ) / @project.get_latest_log_cnt ).to_s + "%" + end + end + # if log is nil then can't figure progress out + return "" + end + + + def get_log_url() + # only when server support log url + if @server.job_log_url.empty? then + return "","" + end + + return "#{@server.job_log_url}/#{@id}/log","" + end + + # + # PROTECTED METHODS + # + protected + + + # main module + def thread_main + @log.info( "New Job #{@id} is started", Log::LV_USER) + + # clean build + if not ReverseBuildChecker.check( self, true ).empty? then + @status = "ERROR" + @log.error( "Reverse-build-check failed!" ) + return + end + + # if this package has compatible OS, check + if @pkg_type == "BINARY" and + @pkginfo.packages[0].os_list.count > 1 then + + pkg = @pkginfo.packages[0] + pkg.os_list.each do |os| + if @os == os then next end + + # skip when the os does not exist in project's supported os list + if not @project.nil? and not @project.os_list.include? os then next end + + # skip when there is higher version of the package + ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") + if not ver_svr.nil? and + Version.new(@pkg_version) <= Version.new(ver_svr) then next end + + # make new package file for compatible OS + newfile = "#{@pkg_name}_#{@pkg_version}_#{os}.zip" + @log.info( "Copying #{@filename} to #{newfile}" ) + FileUtils.cp(@file_path,"#{@source_path}/#{newfile}") + + # reverse check + if not ReverseBuildChecker.check( self, true, os ) then + @status = "ERROR" + @log.error( "Reverse-build-check failed!" ) + return + end + end + end + + # upload + if not upload() then + @status = "ERROR" + return + end + + # INFO. don't change this string + @log.info( "Job is completed!", Log::LV_USER) + @status = "FINISHED" + end + + + # build projects that dependes on me + # can ignore some projects + def check_reverse_build( target_os ) + @log.info( "Checking reverse build dependency ...", Log::LV_USER) + + # get reverse-dependent projects + rev_pkgs = [] + if @pkg_type == "BINARY" then + rev_pkgs += @pkgsvr_client.get_reverse_build_dependent_packages(@pkg_name, target_os) + else + rev_pkgs += @pkgsvr_client.get_reverse_source_dependent_packages(@pkg_name) + end + + rev_projects = @server.prjmgr.get_projects_from_pkgs(rev_pkgs) + + # create reverse build job + rev_build_jobs = [] + rev_projects.each do |p| + prj = p[0] + os = p[1] + version = p[2] + + if prj.type != "GIT" then next end + + # create sub jobs for checking + new_job = prj.create_new_job_from_version(os, version) + new_job.set_rev_build_check_job(self) + + rev_build_jobs.push new_job + end + + # reverse build + if rev_build_jobs.count > 0 then + rev_prjs_txt = rev_build_jobs.map {|j| "#{j.get_project().name}(#{j.os})"}.join(", ") + @log.info( " * Will check reverse-build for next projects: #{rev_prjs_txt}", Log::LV_USER) + end + rev_build_jobs.each do |new_job| + @log.info( " * Checking reverse-build ... #{new_job.get_project().name}(#{new_job.id})", Log::LV_USER) + # job init + result = new_job.init() + # if init is succeeded!, try to execute + if result then + # check available server + rserver = @server.get_available_server( new_job ) + if rserver != nil and rserver != @server then + new_job.set_remote_job( rserver ) + end + # execute + new_job.execute(true) + if new_job.status == "ERROR" then result = false end + end + + # check result + if not result then + return false + end + end + + return true + end + + + def upload() + @log.info( "Uploading ...", Log::LV_USER) + + # get package path list + if @pkg_type == "ARCHIVE" then + binpkg_path_list = Dir.glob("#{@source_path}/#{@pkg_name}") + else + binpkg_path_list = Dir.glob("#{@source_path}/*_*_*.zip") + end + + # upload + u_client = Client.new( @server.pkgserver_url, nil, @log ) + snapshot = u_client.upload( @server.pkgserver_addr, @server.pkgserver_port, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd, binpkg_path_list) + + if snapshot.nil? then + @log.info( "Upload failed...", Log::LV_USER) + + return false + end + + # update local + @log.info( "Upload succeeded. Sync local pkg-server again...", Log::LV_USER) + @pkgsvr_client.update + @log.info("Snapshot: #{snapshot}", Log::LV_USER) + + return true + end + + + # check if local package version is greater than server + def check_package_version() + @log.info( "Checking package version ...", Log::LV_USER) + + # package update + @pkgsvr_client.update + + @pkginfo.packages.each do |pkg| + ver_local = pkg.version + #ver_svr = @pkgsvr_client.get_package_version( pkg.package_name, @os ) + ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") + if not ver_svr.nil? and Version.new(ver_local) <= Version.new(ver_svr) then + @log.error( "Version must be increased : #{ver_local} <= #{ver_svr}", Log::LV_USER) + return false + end + end + + return true + end +end diff --git a/src/build_server/RemoteBuildJob.rb b/src/build_server/RemoteBuildJob.rb index 8e9355b..845a0c8 100644 --- a/src/build_server/RemoteBuildJob.rb +++ b/src/build_server/RemoteBuildJob.rb @@ -1,5 +1,5 @@ =begin - + RemoteBuildJob.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -35,10 +35,9 @@ class RemoteBuildJob < BuildJob attr_accessor :id # initialize - def initialize (id) - super() + def initialize (id,server) + super(id,nil,nil,server) @id = id @type = nil - @outstream = nil end end diff --git a/src/build_server/RemoteBuildServer.rb b/src/build_server/RemoteBuildServer.rb index 07f4fa5..8c71460 100644 --- a/src/build_server/RemoteBuildServer.rb +++ b/src/build_server/RemoteBuildServer.rb @@ -30,13 +30,15 @@ require 'fileutils' $LOAD_PATH.unshift File.dirname(__FILE__) require "RemoteBuildJob.rb" require "BuildComm.rb" +require 'thread' class RemoteBuildServer attr_accessor :ip, :port, :status, :host_os attr_accessor :max_working_jobs, :working_jobs, :waiting_jobs + attr_accessor :pkgserver_url, :path # initialize - def initialize(ip, port) + def initialize(ip, port, parent) @ip = ip @port = port @status = "DISCONNECTED" @@ -44,6 +46,10 @@ class RemoteBuildServer @max_working_jobs = 2 @working_jobs = [] @waiting_jobs = [] + @pkgserver_url = parent.pkgserver_url + @path = "" + @file_transfer_cnt_mutex = Mutex.new + @file_transfer_cnt = 0 end @@ -63,10 +69,10 @@ class RemoteBuildServer def update_state # send - @status = "DISCONNECTED" + #@status = "DISCONNECTED" client = BuildCommClient.create( @ip, @port ) if client.nil? then return end - if client.send("QUERY,SYSTEM") then + if client.send("QUERY|SYSTEM") then result = client.read_lines do |l| tok = l.split(",").map { |x| x.strip } @host_os = tok[0] @@ -83,15 +89,15 @@ class RemoteBuildServer @waiting_jobs = [] client = BuildCommClient.create( @ip, @port ) if client.nil? then return end - if client.send("QUERY,JOB") then + if client.send("QUERY|JOB") then result = client.read_lines do |l| tok = l.split(",").map { |x| x.strip } job_status = tok[0] job_id = tok[1] - new_job = RemoteBuildJob.new(job_id) + new_job = RemoteBuildJob.new(job_id,self) case job_status - when "WAITING" + when "WAITING", "JUST_CREATED", "INITIALIZING" @waiting_jobs.push new_job when "WORKING" @working_jobs.push new_job @@ -123,5 +129,22 @@ class RemoteBuildServer def has_waiting_jobs return (@waiting_jobs.count > 0) end + + + def add_file_transfer() + @file_transfer_cnt_mutex.synchronize { + @file_transfer_cnt += 1 + } + end + + def remove_file_transfer() + @file_transfer_cnt_mutex.synchronize { + @file_transfer_cnt -= 1 + } + end + + def get_file_transfer_cnt() + return @file_transfer_cnt + end end diff --git a/src/build_server/RemoteBuilder.rb b/src/build_server/RemoteBuilder.rb new file mode 100644 index 0000000..0a1ea23 --- /dev/null +++ b/src/build_server/RemoteBuilder.rb @@ -0,0 +1,210 @@ +=begin + + RemoteBuilder.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "utils" +require "PackageManifest" +require "log" + +class RemoteBuilder + attr_accessor :id, :log + + # initialize + def initialize( id, server,ftp_addr, ftp_port, ftp_username, ftp_passwd) + @id = id + @server = server + @addr = server.ip + @port = server.port + @ftp_addr = ftp_addr + @ftp_port = ftp_port + @ftp_username = ftp_username + @ftp_passwd = ftp_passwd + @log = Log.new(nil) + end + + + # build + def build( git_repos, source_path, os, is_rev_build, srcinfo, no_reverse, local_pkgs ) + @log.info( "Start to build on remote server...", Log::LV_USER ) + + # create unique dock number + dock = Utils.create_uniq_name() + + # send local packages + begin + @server.add_file_transfer() + local_pkgs.each do |pkg_path| + @log.info( "Sending file... : #{pkg_path}", Log::LV_USER ) + result = send_file_to_remote( pkg_path, dock ) + if not result then + @log.error( "File transfering failed!", Log::LV_USER ) + @server.remove_file_transfer() + return false + end + end + ensure + @server.remove_file_transfer() + end + + # send build request + @log.info( "Sending build request to remote server...", Log::LV_USER ) + result, result_files = send_build_request(git_repos, os, + is_rev_build, srcinfo, no_reverse, local_pkgs, dock) + + @log.info( "Receiving log file from remote server...", Log::LV_USER ) + if not receive_file_from_remote( "#{source_path}/../remote_log", dock ) then + @log.warn( "File transfering failed! : remote_log", Log::LV_USER ) + end + + if not result then + @log.error( "Building job on remote server failed!", Log::LV_USER ) + return false + end + + # receive binary package + result_files.each do |file_name| + @log.info( "Receiving file from remote server : #{file_name}", Log::LV_USER ) + result = receive_file_from_remote( "#{source_path}/#{file_name}", dock ) + if not result then + @log.error( "File transfering failed! : #{file_name}", Log::LV_USER ) + return false + end + end + + return true + end + + + # upload binary packages that is need to be overwrite + # before remote package + protected + def send_file_to_remote(file_path, dock = "0") + # create client + client = BuildCommClient.create( @addr, @port, @log ) + if client.nil? then + @log.error( "Creating communication client failed!", Log::LV_USER) + return false + end + + # upload file + result = true + file_name = file_path.split("/")[-1] + msg = "UPLOAD|#{dock}" + if client.send( msg ) then + result=client.send_file( @ftp_addr, @ftp_port, @ftp_username, @ftp_passwd, file_path ) + if not result then + @log.error( "File uploading failed...#{file_name}", Log::LV_USER) + end + end + + #close connections + client.terminate + + return result + end + + + # send build request + protected + def send_build_request(git_repos, os, is_rev_build, commit, no_reverse, local_pkgs, dock = "0") + result_files = [] + + client = BuildCommClient.create( @addr, @port, @log ) + if client.nil? then + @log.error( "Creating communication client failed!", Log::LV_USER) + return false, result_files + end + + # get local package names + local_pkg_names = local_pkgs.map { |path| File.basename(path) } + + # send + # format: BUILD|GIT|repository|passwd|os|async|no_reverse|internal|rev-build|commit|pkgs|dock_num + # value : BUILD|GIT|repository| |os|NO |no_reverse|YES |rev-build|commit|pkgs|dock_num + result = true + commit = commit.nil? ? "":commit + pkg_list = local_pkg_names.join(",") + rev = is_rev_build ? "YES":"NO" + msg = "BUILD|GIT|#{git_repos}||#{os}|NO|#{no_reverse}|YES|#{rev}|#{commit}|#{pkg_list}|#{dock}" + if client.send( msg ) then + result = client.read_lines do |l| + # write log first + @log.output( l.strip, Log::LV_USER) + + # check build result + if l.include? "Job is stopped by ERROR" or + l.include? "Error:" then + result = false + break + end + + # gather result files if not reverse build + if not is_rev_build and l =~ /Creating package file \.\.\. (.*)/ then + file_name = $1 + result_files.push file_name + end + + end + end + + # close socket + client.terminate + + return result, result_files + end + + + # receive binary package of remote server + protected + def receive_file_from_remote(file_path, dock = "0") + # create client + client = BuildCommClient.create( @addr, @port, @log ) + if client.nil? then + @log.error( "Creating communication client failed!", Log::LV_USER) + return false + end + + # download file + result = true + file_name = file_path.split("/")[-1] + msg = "DOWNLOAD|#{dock}|#{file_name}" + if client.send( msg ) then + result=client.receive_file( @ftp_addr, @ftp_port, @ftp_username, @ftp_passwd, file_path ) + if not result then + @log.error( "File downloading failed...#{file_name}", Log::LV_USER) + end + end + + #close connections + client.terminate + + return result + end +end diff --git a/src/build_server/ReverseBuildChecker.rb b/src/build_server/ReverseBuildChecker.rb new file mode 100644 index 0000000..7ae943e --- /dev/null +++ b/src/build_server/ReverseBuildChecker.rb @@ -0,0 +1,218 @@ +=begin + + ReverseBuildChecker.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "log" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "utils.rb" +require "client.rb" +require "BuildServer.rb" +require "JobLog.rb" +require "PackageManifest.rb" +require "BuildJob.rb" +require "RegisterPackageJob.rb" + +class ReverseBuildChecker + + # check + def ReverseBuildChecker.check( job, exit_on_error, override_os = nil ) + log = job.log + job_os = (override_os.nil?) ? job.os : override_os + + # start + log.info( "Checking reverse build dependency ...", Log::LV_USER) + + # get target packages that be checked + bin_pkg_name_list = [] + src_pkg_name_list = [] + case job.type + when "BUILD" + job.pkginfo.get_target_packages(job_os).each do |pkg| + bin_pkg_name_list.push pkg.package_name + end + when "REGISTER" + if job.pkg_type == "BINARY" then + bin_pkg_name_list.push job.pkg_name + else + src_pkg_name_list.push job.pkg_name + end + end + + # get reverse projects from build dependency + rev_pkgs = [] + bin_pkg_name_list.each do |pkg_name| + rev_pkgs += job.pkgsvr_client.get_reverse_build_dependent_packages(pkg_name, job_os) + end + src_pkg_name_list.each do |pkg_name| + rev_pkgs += job.pkgsvr_client.get_reverse_source_dependent_packages(pkg_name) + end + rev_pkgs.uniq! + rev_projects = job.server.prjmgr.get_projects_from_pkgs(rev_pkgs) + + # create reverse build job + rev_build_jobs = [] + rev_projects.each do |p| + rev_prj = p[0] + rev_os = p[1] + rev_ver = p[2] + + # if not "GIT" project, ignore it + if rev_prj.type != "GIT" then next end + + # if job on resolve process, its unresolved project + #of pending ancestor must be excluded. + if job.type == "BUILD" and not job.pending_ancestor.nil? then + found = false + job.pending_ancestor.rev_fail_projects.each { |fp| + f_prj = fp[0] + f_os = fp[1] + + if rev_prj == f_prj and rev_os == f_os then + found = true + break + end + } + if found then next end + end + + # if this is sub job, all other sibling job must be excluded + if job.is_sub_job? then + job.get_parent_job().get_sub_jobs.each do |sub_job| + sub_prj = sub_job.get_project() + sub_os = sub_job.os + if rev_prj == sub_prj and rev_os == sub_os then + found = true + break + end + end + if found then next end + end + + # create job + new_job = rev_prj.create_new_job_from_version( rev_os, rev_ver ) + new_job.set_rev_build_check_job( job ) + + rev_build_jobs.push new_job + end + + # reverse build + if rev_build_jobs.count > 0 then + rev_prjs_msg = rev_build_jobs.map {|j| "#{j.get_project().name}(#{j.os})"}.join(", ") + log.info( " * Will check reverse-build for projects: #{rev_prjs_msg}", Log::LV_USER) + end + + # for all reverse job + rev_build_jobs.each do |rev_job| + # add to job manager + job.server.jobmgr.add_reverse_build_job(rev_job) + log.info( " * Added new job for reverse-build ... \ + #{rev_job.get_project().name}(#{rev_job.os}) (#{rev_job.id})", Log::LV_USER) + end + + # wait for job finish + rev_build_finished = false + success_list = [] + failure_list = [] + cancel_other_jobs = false + while not rev_build_finished + rev_build_finished = true + rev_build_jobs.each do |rev_job| + rev_prj = rev_job.get_project() + rev_os = rev_job.os + + case rev_job.status + when "ERROR", "CANCELED" + # add fail list + if not is_project_included?(failure_list, rev_prj, rev_os) then + log.info( " * Reverse-build FAIL ... #{rev_prj.name}(#{rev_os}) (#{rev_job.id})", Log::LV_USER) + failure_list.push [ rev_prj, rev_os ] + write_log_url(log, rev_job) + end + + # if "exist on error" cancel all other jobs + if exit_on_error then + cancel_other_jobs = true + rev_build_jobs.each do |j| + if j.status != "ERROR" and j.status != "FINISHED" and + j.status != "CANCELED" and j.cancel_state == "NONE" then + + j.cancel_state = "INIT" + end + end + break + end + when "FINISHED" + # add success list + if not success_list.include? rev_job then + log.info( " * Reverse-build OK ... #{rev_prj.name}(#{rev_os}) (#{rev_job.id})", Log::LV_USER) + success_list.push rev_job + end + else + rev_build_finished = false + end + end + + sleep 1 + end + + # clean up all reverse build jobs + rev_build_jobs.each do |rev_job| + if rev_job.status == "ERROR" or rev_job.status == "CANCELED" then + rev_job.server.cleaner.clean_afterwards(rev_job.id) + else + rev_job.server.cleaner.clean(rev_job.id) + end + end + + return failure_list + end + + + private + def self.is_project_included?( prj_list, prj, os ) + prj_list.each do |p| + if p[0] == prj and p[1] == os then return true end + end + + return false + end + + + # write web url for log + private + def self.write_log_url(log, job) + url,remote_url = job.get_log_url() + if not url.empty? then + log.info( " ** Log1: #{url}", Log::LV_USER) + end + if not remote_url.empty? then + log.info( " ** Log2: #{remote_url}", Log::LV_USER) + end + end +end diff --git a/src/build_server/SocketJobRequestListener.rb b/src/build_server/SocketJobRequestListener.rb index f7656a5..ae438a9 100644 --- a/src/build_server/SocketJobRequestListener.rb +++ b/src/build_server/SocketJobRequestListener.rb @@ -27,8 +27,6 @@ Contributors: =end $LOAD_PATH.unshift File.dirname(__FILE__) -require "GitBuildJob.rb" -require "LocalBuildJob.rb" require "JobLog.rb" require "BuildComm.rb" @@ -40,13 +38,22 @@ class SocketJobRequestListener @parent_server = parent @thread = nil @finish_loop = false + @comm_server = nil @log = @parent_server.log end # start listening def start() @thread = Thread.new { - main() + # make loop recover when unhandled exception occurred + while not @finish_loop + begin + main() + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + end } end @@ -63,21 +70,26 @@ class SocketJobRequestListener def main() # server open begin - server = BuildCommServer.new(@parent_server.port, @log) + ftp_url = Utils.generate_ftp_url(@parent_server.ftp_addr, @parent_server.ftp_port, + @parent_server.ftp_username, @parent_server.ftp_passwd) + cache_dir = "#{@parent_server.transport_path}/.cache" + @comm_server = BuildCommServer.create(@parent_server.port, @log, ftp_url, cache_dir) rescue @log.info "Server creation failed" + puts "Server creation failed" + @parent_server.stop return end # loop @log.info "Entering Control Listening Loop ... " @finish_loop = false - server.wait_for_connection(@finish_loop) do |req| + @comm_server.wait_for_connection(@finish_loop) do |req| handle_job_request( req ) end # quit - server.terminate + @comm_server.terminate end @@ -99,8 +111,8 @@ class SocketJobRequestListener # parse request cmd = "" - if req_line.split(",").count > 0 then - cmd = req_line.split(",")[0].strip + if req_line.split("|").count > 0 then + cmd = req_line.split("|")[0].strip end case cmd @@ -110,8 +122,36 @@ class SocketJobRequestListener handle_cmd_resolve( req_line, req ) when "QUERY" handle_cmd_query( req_line, req ) + when "CANCEL" + handle_cmd_cancel( req_line, req ) when "STOP" handle_cmd_stop( req_line, req ) + when "UPGRADE" + handle_cmd_upgrade( req_line, req ) + when "FULLBUILD" + handle_cmd_fullbuild( req_line, req ) + when "REGISTER" + handle_cmd_register( req_line, req ) + when "DOWNLOAD" + Thread.new { + begin + handle_cmd_download( req_line, req ) + rescue => e + @log.error "Transfering file failed!" + @log.error e.message + @log.error e.backtrace.inspect + end + } + when "UPLOAD" + Thread.new { + begin + handle_cmd_upload( req_line, req ) + rescue => e + @log.error "Transfering file failed!" + @log.error e.message + @log.error e.backtrace.inspect + end + } else @log.info "Received Unknown REQ: #{req_line}" raise "Unknown request: #{req_line}" @@ -122,87 +162,205 @@ class SocketJobRequestListener # "BUILD" def handle_cmd_build( line, req ) - tok = line.split(",").map { |x| x.strip } - if tok.count < 4 then + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then @log.info "Received Wrong REQ: #{line}" raise "Invalid request format is used: #{line}" end - case tok[1] - # BUILD,GIT,repos,commit,os,url,async - when "GIT" - @log.info "Received BUILD GIT => #{tok[2]}" + # check type + if tok[1] != "GIT" then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end - # check asynchronous job - async = (not tok[6].nil? and tok[6]=="YES" ? true:false) - if async then - new_job = GitBuildJob.new( tok[2], tok[3], tok[4], tok[5], [], @parent_server, nil, nil, false) - else - new_job = GitBuildJob.new( tok[2], tok[3], tok[4], tok[5], [], @parent_server, nil, req, false) + # Case1. BUILD|GIT|project_name|passwd|os_list|async|no_reverse + # Case2. BUILD|GIT|git_repos||os|async|no_reverse|internal|rev_build|commit|pkgs|dock_num + + # parse + project_name_list = tok[2].split(",") + passwd_list = tok[3].split(",") + passwd = passwd_list[0] + os_list = tok[4].split(",") + async = tok[5].eql? "YES" + no_reverse = tok[6].eql? "YES" + is_internal = tok[7].eql? "YES" + rev_job = tok[8].eql? "YES" + git_commit = (not tok[9].nil? and not tok[9].empty?) ? tok[9] : nil + pkg_files = (not tok[10].nil? and not tok[10].empty?) ? tok[10].split(",") : [] + dock_num = (not tok[11].nil? and not tok[11].empty?) ? tok[11].strip : "0" + + # check supported os if not internal job + if not is_internal then + os_list = check_supported_os( os_list , req ) + if os_list.nil? or os_list.empty? then + raise "Unsupported OS name is used!" end - BuildCommServer.send_begin(req) + end - # start job. If log url is supported, show it - if not @parent_server.job_log_url.empty? then - new_job.log.info( "Added new job \"#{new_job.id}\"! Check following URL", Log::LV_USER) - new_job.log.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + # multi build job + if project_name_list.count > 1 or os_list.count > 1 then + new_job_list = [] + i = 0 + project_name_list.each { |pname| + if not passwd_list[i].nil? then passwd = passwd_list[i] + else passwd = passwd_list[0] end + check_build_project(pname,passwd,req) + os_list.each { |os| + new_job = create_new_job( pname, os ) + if new_job.nil? then + @log.warn "\"#{pname}\" does not support #{os}" + next + end + new_job_list.push new_job + @log.info "Received a request for building this project : #{pname}, #{os}" + } + i = i + 1 + } + + if new_job_list.count > 1 then + new_job = @parent_server.prjmgr.create_new_multi_build_job( new_job_list ) + elsif new_job_list.count == 1 then + new_job = new_job_list[0] else - new_job.log.info( "Added new job \"#{new_job.id}\"!", Log::LV_USER) + raise "Multi-Build Job creation failed!" end - # if asynchronouse, quit connection - if async then - if not @parent_server.job_log_url.empty? then - req.puts( "Info: Added new job \"#{new_job.id}\"! Check following URL") - req.puts( "Info: * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log") - else - req.puts( "Info: Added new job \"#{new_job.id}\"!") - end + # transfered job + elsif is_internal then + git_repos = project_name_list[0] + os = os_list[0] - BuildCommServer.send_end(req) - BuildCommServer.disconnect(req) - end + new_job = create_new_internal_job(git_repos, os, git_commit, pkg_files, dock_num ) + if rev_job then new_job.set_rev_build_check_job(nil) end - # add - @parent_server.jobmgr.add_job( new_job ) + # single job + elsif project_name_list.count == 1 and os_list.count == 1 then + pname = project_name_list[0] + os = os_list[0] - # BUILD,LOCAL,path,os,url - when "LOCAL" - @log.info "Received BUILD LOCAL => #{tok[2]}" - - BuildCommServer.send_begin(req) - @parent_server.jobmgr.add_job( - LocalBuildJob.new( tok[2], tok[3], tok[4], [], @parent_server, nil, req, false)) + check_build_project(pname,passwd,req) + new_job = create_new_job( pname, os ) else - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" + BuildCommServer.send_begin(req) + req.puts "Error: There is no valid job to build!" + BuildCommServer.send_end(req) + raise "No valid jobs!" + end + + if no_reverse then new_job.set_no_reverse end + + # create logger and set + logger = JobLog.new( new_job, req ) + if not async then new_job.set_logger(logger) end + logger.init + + # notify that job has been received + logger.info( "Added new job \"#{new_job.id}\" for #{new_job.os}!", Log::LV_USER) + if not @parent_server.job_log_url.empty? then + logger.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + end + + # if asynchronouse, quit connection + if async then + logger.info( "Above job(s) will be processed asynchronously!", Log::LV_USER) + logger.close end + + # add to job queue + if new_job.is_rev_build_check_job() then + @parent_server.jobmgr.add_reverse_build_job( new_job ) + else + @parent_server.jobmgr.add_job( new_job ) + end + end + + + def check_build_project(prj_name, passwd, req) + # check project + prj = check_project_exist(prj_name, req) + if prj.nil? then + raise "Requested project does not exist!" + end + + # check passwd + if not check_project_password(prj, passwd, req) then + raise "Project's password is not matched!!" + end + + # check project type + if prj.type == "BINARY" then + BuildCommServer.send_begin(req) + req.puts "Can't build about Binary type package." + BuildCommServer.send_end(req) + raise "Can't build about Binary type package." + end end # "RESOLVE" def handle_cmd_resolve( line ,req) - tok = line.split(",").map { |x| x.strip } - if tok.count < 4 then + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then @log.info "Received Wrong REQ: #{line}" raise "Invalid request format is used: #{line}" end case tok[1] - # RESOLVE,GIT,repos,commit,os,url + # RESOLVE|GIT|repos|commit|os|async when "GIT" - @log.info "Received RESOLVE GIT => #{tok[2]}" + # parse + project_name=tok[2] + passwd=tok[3] + os=tok[4] + async = tok[5].eql? "YES" + + # check project + prj = check_project_exist(project_name, req) + if prj.nil? then + raise "Requested project does not exist!" + end + + # check passwd + if not check_project_password(prj, passwd, req) then + raise "Project's password is not matched!!" + end - BuildCommServer.send_begin(req) - @parent_server.jobmgr.add_job( - GitBuildJob.new( tok[2], tok[3], tok[4], tok[5], [], @parent_server, nil, req, true)) - # RESOLVE,LOCAL,path,os,url - when "LOCAL" - @log.info "Received RESOLVE LOCAL => #{tok[2]}" + # check os + os_list = check_supported_os( os , req ) + if os_list.nil? or os_list.empty? then + raise "Unsupported OS name is used!" + end + os = os_list[0] + + # create new job + new_job = create_new_job( project_name, os ) + if new_job.nil? then + raise "Creating build job failed : #{project_name}, #{os}" + end + @log.info "Received a request for resolving this project : #{project_name}, #{os}" - BuildCommServer.send_begin(req) - @parent_server.jobmgr.add_job( - LocalBuildJob.new( tok[2], tok[3], tok[4], [], @parent_server, nil, req, true)) + # resolve + new_job.set_resolve_flag() + + # create logger and set + logger = JobLog.new( new_job, req ) + if not async then new_job.set_logger(logger) end + logger.init + + # notify that job has been received + logger.info( "Added new job \"#{new_job.id}\" for #{new_job.os}!", Log::LV_USER) + if not @parent_server.job_log_url.empty? then + logger.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + end + + # if asynchronouse, quit connection + if async then + logger.info( "Above job(s) will be processed asynchronously!", Log::LV_USER) + logger.close + end + + @parent_server.jobmgr.add_job( new_job ) else @log.info "Received Wrong REQ: #{line}" raise "Invalid request format is used: #{line}" @@ -212,27 +370,58 @@ class SocketJobRequestListener # "QUERY" def handle_cmd_query( line, req ) - tok = line.split(",").map { |x| x.strip } + tok = line.split("|").map { |x| x.strip } if tok.count < 2 then @log.info "Received Wrong REQ: #{line}" raise "Invalid request format is used: #{line}" end case tok[1] + + # QUERY, FTP + when "FTP" + BuildCommServer.send_begin(req) + BuildCommServer.send(req,"#{@parent_server.ftp_addr},#{@parent_server.ftp_username},#{@parent_server.ftp_passwd}") + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + # QUERY,JOB when "JOB" #puts "Received QUERY JOB" + # gather all jobs to show + job_list = @parent_server.jobmgr.jobs + @parent_server.jobmgr.internal_jobs + @parent_server.jobmgr.reverse_build_jobs + + # send the status BuildCommServer.send_begin(req) - for job in @parent_server.jobmgr.get_working_jobs - BuildCommServer.send(req,"WORKING,#{job.id},#{job.pkginfo.packages[0].source}") - end - for job in @parent_server.jobmgr.get_waiting_jobs - BuildCommServer.send(req,"WAITING,#{job.id},#{job.pkginfo.packages[0].source}") - end - for job in @parent_server.jobmgr.get_remote_jobs - BuildCommServer.send(req,"REMOTE ,#{job.id},#{job.pkginfo.packages[0].source}") + job_list.each do |job| + status = job.status + if status == "REMOTE_WORKING" then status = "REMOTE" end + if job.cancel_state != "NONE" then status = "CANCEL" end + + case job.type + when "BUILD" + if status == "PENDING" then + if job.pending_ancestor.nil? then + ids = "/" + else + ids = job.pending_ancestor.id + end + BuildCommServer.send(req,"#{status}:#{ids},#{job.id},#{job.get_project().name},#{job.os} #{job.progress}") + else + BuildCommServer.send(req,"#{status},#{job.id},#{job.get_project().name},#{job.os} #{job.progress}") + end + when "REGISTER" + if job.pkg_type == "BINARY" and not job.get_project().nil? then + BuildCommServer.send(req,"#{status},#{job.id},#{job.get_project().name},#{job.os} #{job.progress}") + else + BuildCommServer.send(req,"#{status},#{job.id},#{job.pkg_name}") + end + when "MULTIBUILD" + BuildCommServer.send(req,"#{status},#{job.id},MULTI-BUILD : #{job.sub_jobs.map{|x| x.id}.join(" ")}") + end end + BuildCommServer.send_end(req) BuildCommServer.disconnect(req) @@ -244,6 +433,45 @@ class SocketJobRequestListener BuildCommServer.send(req,"#{@parent_server.host_os},#{@parent_server.jobmgr.max_working_jobs}") BuildCommServer.send_end(req) BuildCommServer.disconnect(req) + when "PROJECT" + BuildCommServer.send_begin(req) + # print GIT projects + sorted_list = @parent_server.prjmgr.projects.sort { |x,y| x.name <=> y.name } + sorted_list.each do |prj| + if prj.type != "GIT" then next end + BuildCommServer.send(req,"G,#{prj.name},#{prj.repository},#{prj.branch}") + end + # print BINARY projects + sorted_list.each do |prj| + if prj.type != "BINARY" then next end + BuildCommServer.send(req,"B,#{prj.name},#{prj.pkg_name}") + end + # print REMOTE project + sorted_list.each do |prj| + if prj.type != "REMOTE" then next end + BuildCommServer.send(req,"R,#{prj.name}") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + + when "OS" + BuildCommServer.send_begin(req) + # print GIT projects + @parent_server.supported_os_list.each do |os_name| + BuildCommServer.send(req,"#{os_name}") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + + when "FRIEND" + BuildCommServer.send_begin(req) + # print GIT projects + @parent_server.friend_servers.each do |server| + BuildCommServer.send(req,"#{server.status},#{server.host_os},#{server.waiting_jobs.length},#{server.working_jobs.length},#{server.max_working_jobs},#{server.get_file_transfer_cnt}") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + else @log.info "Received Wrong REQ: #{line}" raise "Invalid request format is used: #{line}" @@ -251,9 +479,83 @@ class SocketJobRequestListener end + # "CANCEL" + def handle_cmd_cancel( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + cancel_job = nil + + #CANCEL, JOB + @parent_server.jobmgr.jobs.each do |j| + if "#{j.id}" == "#{tok[1]}" then + cancel_job = j + break + end + end + + BuildCommServer.send_begin(req) + if cancel_job.nil? then + BuildCommServer.send(req, "There is no job \"#{tok[1]}\"") + raise "There is no job \"#{tok[1]}\"" + else + if cancel_job.cancel_state == "NONE" then + # check passwd + if cancel_job.type == "MULTIBUILD" then + cancel_job.sub_jobs.select{|x| x.cancel_state == "NONE" }.each do |sub| + if not check_project_password( sub.get_project, tok[2], req) then + BuildCommServer.send(req, "Project's password is not matched!!") + raise "Project's password is not matched!!" + end + end + + BuildCommServer.send(req, "\"#{cancel_job.id}, #{cancel_job.sub_jobs.map{|x| x.id}.join(", ")}\" will be canceled") + cancel_job.cancel_state = "INIT" + else + if not check_project_password( cancel_job.get_project, tok[2], req) then + BuildCommServer.send(req, "Project's password is not matched!!") + raise "Project's password is not matched!!" + else + BuildCommServer.send(req, "\"#{cancel_job.id}\" will be canceled") + cancel_job.cancel_state = "INIT" + end + end + else + BuildCommServer.send(req, "\"#{cancel_job.id}\" is already canceled") + end + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + # "STOP" def handle_cmd_stop( line, req ) - tok = line.split(",").map { |x| x.strip } + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + + BuildCommServer.send_begin(req) + if tok[1] != @parent_server.password then + BuildCommServer.send(req,"Password mismatched!") + else + BuildCommServer.send(req,"Server will be down!") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + if tok[1] == @parent_server.password then + @parent_server.finish = true + end + end + + + # "UPGRADE" + def handle_cmd_upgrade( line, req ) + tok = line.split("|").map { |x| x.strip } if tok.count < 2 then @log.info "Received Wrong REQ: #{line}" raise "Invalid request format is used: #{line}" @@ -269,6 +571,306 @@ class SocketJobRequestListener BuildCommServer.disconnect(req) if tok[1] == @parent_server.password then @parent_server.finish = true + @parent_server.upgrade = true + end + end + + + # "FULLBUILD" + def handle_cmd_fullbuild( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + + server_passwd = tok[1] + + # check server password + if server_passwd != @parent_server.password then + BuildCommServer.send_begin(req) + BuildCommServer.send(req,"Password mismatched!") + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + else + # create full build job + new_job = @parent_server.prjmgr.create_new_full_build_job() + + # set logger + logger = JobLog.new( new_job, req ) + new_job.set_logger(logger) + logger.init + + # add to job + @parent_server.jobmgr.add_job( new_job ) + end + end + + + # "REGISTER" + def handle_cmd_register( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 4 then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + + type = tok[1] + + case type + # REGISTER|BINARY-LOCAL|local_path + # REGISTER|SOURCE-LOCAL|local_path + when "BINARY-LOCAL", "SOURCE-LOCAL" + file_path = tok[2] + new_job = @parent_server.jobmgr.create_new_register_job( file_path ) + logger = JobLog.new( new_job, req ) + new_job.set_logger(logger) + logger.init + + # add + @parent_server.jobmgr.add_job( new_job ) + + # REGISTER|BINARY|filename|passwd + when "BINARY" + # parse + filename = tok[2] + passwd = tok[3] + dock = (tok[4].nil? or tok[4].empty?) ? "0" : tok[4].strip + + # check project + prj = check_project_for_package_file_name(filename, req) + if prj.nil? then + raise "No project is defined for this binary : #{filename}!" + end + + # check passwd + if not check_project_password(prj, passwd, req) then + raise "Project's password is not matched!!" + end + + # create new job + @log.info "Received a request for uploading binaries : #{filename}" + new_job = create_new_upload_job( prj.name, filename, dock, req ) + if new_job.nil? then + raise "Creating build job failed : #{prj.name}, #{filename}" + end + + # create logger and set + logger = JobLog.new( new_job, req ) + new_job.set_logger(logger) + logger.init + + # notify that job has been received + logger.info( "Added new job \"#{new_job.id}\" for #{new_job.os}!", Log::LV_USER) + if not @parent_server.job_log_url.empty? then + logger.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + end + + # add + @parent_server.jobmgr.add_job( new_job ) + else + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + + end + + + # "UPLOAD" + def handle_cmd_upload( line, req ) + @log.info "Received File transfer REQ : #{line}" + + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + + dock_num = tok[1].strip + + BuildCommServer.send_begin(req) + incoming_dir = "#{@parent_server.transport_path}/#{dock_num}" + if not File.exist? incoming_dir then FileUtils.mkdir_p incoming_dir end + @comm_server.receive_file( req, incoming_dir ) + BuildCommServer.send_end(req) + end + + + # "DOWNLOAD" + # format = DOWNLOAD|dock_num|file_name + def handle_cmd_download( line, req ) + @log.info "Received File transfer REQ : #{line}" + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then + @log.info "Received Wrong REQ: #{line}" + raise "Invalid request format is used: #{line}" + end + + dock_num = tok[1].strip + file_name = tok[2] + + @log.info "Received a request for download file : #{file_name}" + outgoing_dir = "#{@parent_server.transport_path}/#{dock_num}" + BuildCommServer.send_begin(req) + @log.info "Sending requested file...: #{file_name}" + @comm_server.send_file(req, "#{outgoing_dir}/#{file_name}") + # remove file if "dock" defined + if dock_num != "0" then + @log.info "Removing requested file...: #{file_name}" + FileUtils.rm_rf "#{outgoing_dir}/#{file_name}" + if Utils.directory_emtpy?(outgoing_dir) then + FileUtils.rm_rf "#{outgoing_dir}" + end + end + + BuildCommServer.send_end(req) + end + + + + private + def check_project_exist(project_name, req) + prj = @parent_server.prjmgr.get_project(project_name) + if prj.nil? then + BuildCommServer.send_begin(req) + req.puts "Error: Requested project does not exist!" + req.puts "Info: Check project name using \"query\" command option !" + BuildCommServer.send_end(req) + return nil + end + + return prj + end + + private + def check_project_for_package_file_name(filename, req) + # get package name + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + pkg_name = new_name.split(",")[0] + + prj = @parent_server.prjmgr.get_project_from_package_name(pkg_name) + if prj.nil? then + BuildCommServer.send_begin(req) + req.puts "Error: Requested project does not exist!" + req.puts "Info: Check project name using \"query\" command option !" + BuildCommServer.send_end(req) + return nil + end + + return prj + end + + + private + def check_project_password(prj, passwd, req) + + if prj.is_passwd_set? and not prj.passwd_match?(passwd) then + BuildCommServer.send_begin(req) + req.puts "Error: Project's password is not matched!" + req.puts "Error: Use -w option to input your project password" + BuildCommServer.send_end(req) + return false + end + + return true + end + + + private + def check_supported_os(os_list, req) + + # check if supported os list contain at least one OS + if @parent_server.supported_os_list.empty? then + BuildCommServer.send_begin(req) + req.puts "Error: There is no OS supported by the build server." + BuildCommServer.send_end(req) + return nil + end + + result = [] + os_list.each do |os| + if os == "all" or os == "*" then + result = result + @parent_server.supported_os_list + + elsif os == "default" then + os = @parent_server.supported_os_list[0] + result.push os + @log.info "The default OS \"#{os}\" is used as target OS" + + elsif os.include? "*" then + reg_os = os.gsub("*","[a-zA-Z0-9.]*") + @parent_server.supported_os_list.each do |svr_os| + matches = svr_os.match("#{reg_os}") + if not matches.nil? and matches.size == 1 and + matches[0] == svr_os then + result.push svr_os + end + end + else + if not @parent_server.supported_os_list.include?(os) then + BuildCommServer.send_begin(req) + req.puts "Error: Unsupported OS name \"#{os}\" is used!" + req.puts "Error: Check the following supported OS list. " + @parent_server.supported_os_list.each do |os_name| + req.puts " * #{os_name}" + end + BuildCommServer.send_end(req) + return nil + else + result.push os + end + end + end + + if result.empty? then + BuildCommServer.send_begin(req) + req.puts "Error: There is no OS supported by the build server." + BuildCommServer.send_end(req) + return nil + end + + result.uniq! + + return result + end + + + private + def create_new_job( project_name, os ) + return @parent_server.prjmgr.create_new_job(project_name, os) + end + + + private + def create_new_upload_job( project_name, filename, dock, req) + + new_job = @parent_server.prjmgr.get_project(project_name).create_new_job(filename, dock) + + if new_job.nil? then + BuildCommServer.send_begin(req) + req.puts "Error: Creating job failed: #{project_name} #{filename}" + BuildCommServer.send_end(req) + return nil end + + return new_job + end + + + private + def create_new_internal_job( git_repos, os, git_commit, pkg_files, dock_num ) + prj = @parent_server.prjmgr.get_git_project( git_repos ) + if prj.nil? then + prj = @parent_server.prjmgr.create_unnamed_git_project( git_repos ) + end + new_job = prj.create_new_job(os) + new_job.set_internal_job( dock_num ) + new_job.set_git_commit(git_commit) + incoming_dir = "#{@parent_server.transport_path}/#{dock_num}" + pkg_files.each { |file| + new_job.add_external_package( file ) + } + + return new_job end end diff --git a/src/builder/Builder.rb b/src/builder/Builder.rb index a9e672c..f0087fe 100644 --- a/src/builder/Builder.rb +++ b/src/builder/Builder.rb @@ -36,22 +36,24 @@ require "log" class Builder private_class_method :new - attr_accessor :id, :pkgserver_url, :log + attr_accessor :id, :pkgserver_url, :log, :buildroot_dir, :cache_dir CONFIG_ROOT = Utils::HOME + "/.build_tools/builder" @@instance_map = {} # initialize - def initialize (id, pkgserver_url, log_path) + def initialize (id, pkgserver_url, log_path, buildroot_dir, cache_dir) @id = id @pkgserver_url = pkgserver_url @host_os = Utils::HOST_OS + @buildroot_dir = buildroot_dir + @cache_dir = cache_dir @log = Log.new(log_path) end # create - def self.create (id, pkgserver_url, log_path) + def self.create (id, pkgserver_url, log_path, buildroot_dir, cache_dir) # check builder config root check_builder_config_root @@ -61,8 +63,24 @@ class Builder FileUtils.rm_rf "#{CONFIG_ROOT}/#{id}" end + # create buildroot if not set + if buildroot_dir.nil? then + buildroot_dir = "#{CONFIG_ROOT}/#{id}/buildroot" + if not File.exist? buildroot_dir then + FileUtils.mkdir_p buildroot_dir + end + end + + # create cachedir if not set + if cache_dir.nil? then + cache_dir = "#{CONFIG_ROOT}/#{id}/build_cache" + if not File.exist? cache_dir then + FileUtils.mkdir_p cache_dir + end + end + # create new instance and return it - @@instance_map[id] = new( id, pkgserver_url, log_path ) + @@instance_map[id] = new( id, pkgserver_url, log_path, buildroot_dir, cache_dir ) # write config write_builder_config( @@instance_map[id] ) @@ -83,6 +101,18 @@ class Builder end + def self.exist?( id ) + # check builder config root + check_builder_config_root + + # check id + if File.exist? "#{CONFIG_ROOT}/#{id}" then + return true + else + return false + end + end + # get def self.get( id ) @@ -106,55 +136,59 @@ class Builder # clean def clean( src_path ) - build_root_dir = "#{CONFIG_ROOT}/#{@id}/buildroot" - - # create pkginfo - pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") - - # make clean - for pkg in pkginfo.packages - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.linux" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.windows" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.darwin" - end - env_def = - "SRCDIR=\"#{src_path}\" " - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} clean" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on clean script", Log::LV_USER ) - return false - end - - return true + return clean_project_directory( src_path, nil ) end # build - def build( src_path, os, clean, reverse_dep_check, pending_pkg_dir_list, ignore_rev_dep_build_list ) + def build( src_path, os, clean, local_pkgs, is_local_build ) # create pkginfo - pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") - - # check there are packages which can be built - if not pkginfo.package_exist?(os, Utils::HOST_OS ) then - @log.error( "There are no packages which can be built on this host OS: #{Utils::HOST_OS}") - @log.error( " * Check \"Build-host-os\" in pkginfo.manifest" ) + if not File.exist? "#{src_path}/package/pkginfo.manifest" then + @log.error( "The \"package/pkginfo.manifest\" file does not exist!", Log::LV_USER) return false end - # set build root - if clean then - build_root_dir = "#{CONFIG_ROOT}/#{@id}/temp_root" - else - build_root_dir = "#{CONFIG_ROOT}/#{@id}/buildroot" + # read pkginfo + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + return false end - FileUtils.mkdir_p build_root_dir + # set default build os + build_host_os = @host_os - local_pkg_list = [] - pending_pkg_dir_list.each do |dir| - local_pkg_list += Dir.entries(dir).select{|e| e =~ /\.zip$/}.map{|p| dir + "/" + p} - end + # check there are packages which can be built + if not pkginfo.package_exist?(os, build_host_os ) then + if is_local_build and File.exist? "#{src_path}/package/pkginfo.manifest.local" then + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest.local") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end + if not pkginfo.package_exist?(os, build_host_os ) then + + @log.error( "This project does not support a build on this host OS: #{build_host_os}") + @log.error( " * Check \"Build-host-os\" in pkginfo.manifest or pkginfo.manifest.local" ) + + return false + end + else + @log.error( "This project does not support a build on this host OS: #{build_host_os}") + @log.error( " * Check \"Build-host-os\" in pkginfo.manifest" ) + + return false + end + end + + # set build root + build_root_dir = @buildroot_dir + if not File.exist? build_root_dir then + FileUtils.mkdir_p build_root_dir + end # create client @log.info( "Downloding client is initializing...", Log::LV_USER) @@ -162,12 +196,17 @@ class Builder if clean then cl.clean(true) end - cl.update + + # get local repository path list + repos_paths = [] + local_pkgs.each { |path| + repos_paths.push File.dirname(path) + } + repos_paths.uniq! # install build dependencies - package_overwrite_list = [] @log.info( "Installing dependent packages...", Log::LV_USER) - pkginfo.get_build_dependencies( os, @host_os ).each do |dep| + pkginfo.get_build_dependencies( os ).each do |dep| if dep.target_os_list.count != 0 then dep_target_os = dep.target_os_list[0] else @@ -177,73 +216,52 @@ class Builder # get local dependent package pkgexp = Regexp.new("\/#{dep.package_name}_.*_#{dep_target_os}\.zip$") - package_overwrite_list += local_pkg_list.select{|l| l =~ pkgexp} - - if not cl.install(dep.package_name, dep_target_os, true, false) then - @log.error( "Installing \"#{dep.package_name}\" failed!", Log::LV_USER) - return false + local_dep_pkgs = local_pkgs.select{|l| l =~ pkgexp} + + # install package from remote package server + if local_dep_pkgs.empty? then + if not cl.install(dep.package_name, dep_target_os, true, false) then + @log.error( "Installing \"#{dep.package_name}\" failed!", Log::LV_USER) + return false + end + else + local_dep_pkgs.each do |l| + @log.info( "Installing local pacakge...#{l}", Log::LV_USER) + if not File.exist? l then + @log.error( "File not found!: #{l}", Log::LV_USER ) + end + cl.install_local_pkg(l,true,false, repos_paths) + end end end - # overwrite local dependent packages - package_overwrite_list.each do |l| - cl.install_local_pkg(l,false) - end - @log.info( "Downloading dependent source packages...", Log::LV_USER) - pkginfo.get_source_dependencies(os,@host_os).each do |dep| - @log.info( " * #{dep.package_name}", Log::LV_USER) - - if cl.download_dep_source(dep.package_name).nil? then - @log.error( "Downloading \"#{dep.package_name}\" failed!", Log::LV_USER) + src_archive_list = [] + pkginfo.get_source_dependencies(os,build_host_os).each do |dep| + src_archive_list.push dep.package_name + end + src_archive_list.uniq! + src_archive_list.each do |archive_name| + @log.info( " * #{archive_name}", Log::LV_USER) + if cl.download_dep_source(archive_name).nil? then + @log.error( "Downloading \"#{archive_name}\" failed!", Log::LV_USER) return false end - end + end # make clean @log.info( "Make clean...", Log::LV_USER) - for pkg in pkginfo.packages - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.linux" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.windows" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.darwin" - end - - # convert path if windows - if Utils::HOST_OS == "windows" then - build_root_dir = Utils.get_unix_path( build_root_dir ) - end - env_def = - "BUILD_TARGET_OS=#{os} \ - SRCDIR=\"#{src_path}\" \ - ROOTDIR=\"#{build_root_dir}\" " - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} clean" - - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on clean script", Log::LV_USER) + if not clean_project_directory( src_path, os ) then return false end - # make source package - @log.info( "Make source package...", Log::LV_USER) - build_command = "cd \"#{src_path}\";tar czf #{pkginfo.packages[0].source}_#{pkginfo.packages[0].version}.tar.gz --exclude=.git *" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on tar script", Log::LV_USER) - return false - end - - # execute build script @log.info( "Make build...", Log::LV_USER) - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} build" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on build script", Log::LV_USER) + if not execute_build_command("build", src_path, build_root_dir, os, pkginfo.get_version) then return false end - # execute install script @log.info( "Make install...", Log::LV_USER) - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} install" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on build script", Log::LV_USER) + if not execute_build_command("install", src_path, build_root_dir, os, pkginfo.get_version) then return false end @@ -256,37 +274,12 @@ class Builder # zip @log.info( "Zipping...", Log::LV_USER) - make_zip(pkginfo,os,src_path) - - # check reverse dependecy if needed - if reverse_dep_check then - if not check_reverse_build_dependency_fail_list(src_path, os, cl, true, ignore_rev_dep_build_list).empty? then - return false - end + if not make_zip(pkginfo,os,src_path) then + @log.error( "Creating packages failed!", Log::LV_USER) + return false end - return true - end - def build_resolve(src_path, os, pending_pkg_dir_list, ignore_rev_dep_build_list) - # clean build - if not build(src_path, os, true, false, pending_pkg_dir_list, ignore_rev_dep_build_list) then - return nil - end - # init client - build_root_dir = "#{CONFIG_ROOT}/#{@id}/temp_root" - cl = Client.new(@pkgserver_url, build_root_dir, @log) - # rev build - return check_reverse_build_dependency_fail_list(src_path, os, cl, false, ignore_rev_dep_build_list ) - end - - - # reset - def reset() - build_root_dir = "#{CONFIG_ROOT}/#{@id}/buildroot" - temp_dir = cl.location - cl.location = build_root_dir - cl.clean(true) - cl.location = temp_dir + return true end @@ -307,15 +300,17 @@ class Builder def self.write_builder_config( obj ) # create config folder builder_dir = "#{CONFIG_ROOT}/#{obj.id}" - FileUtils.mkdir_p( "#{builder_dir}" ) + if not File.exist? builder_dir then + FileUtils.mkdir_p( "#{builder_dir}" ) + end # write configuration File.open( "#{builder_dir}/builder.cfg", "w" ) do |f| f.puts "ID=#{obj.id}" f.puts "PSERVER_URL=#{obj.pkgserver_url}" f.puts "LOG-PATH=#{obj.log.path}" + f.puts "CACHE-DIR=#{obj.cache_dir}" end - puts "#{builder_dir}/builder.cfg" end @@ -326,6 +321,8 @@ class Builder # read configuration builder_dir = "#{CONFIG_ROOT}/#{id}" log_path = nil + cache_dir = "#{CONFIG_ROOT}/#{id}/build_cache" + buildroot_dir = "#{CONFIG_ROOT}/#{id}/buildroot" File.open( "#{builder_dir}/builder.cfg", "r" ) do |f| f.each_line do |l| if l.start_with?("PSERVER_URL=") @@ -333,6 +330,10 @@ class Builder elsif l.start_with?("LOG-PATH=") log_path = l.split("=")[1].strip log_path = nil if log_path == "STDOUT" + elsif l.start_with?("CACHE-DIR=") + cache_dir = l.split("=")[1].strip + elsif l.start_with?("BUILDROOT-DIR=") + buildroot_dir = l.split("=")[1].strip else next end @@ -340,151 +341,137 @@ class Builder end if log_path.empty? then log_path = nil end + # create object & return it - return new( id, pkgserver_url, log_path ) + return new( id, pkgserver_url, log_path, buildroot_dir, cache_dir ) end - # check reverse build dependency - def check_reverse_build_dependency_fail_list( parent_path, os, pkg_cl, immediately, ignore_rev_dep_build_list ) - @log.info( "Checking reverse build dependency ...", Log::LV_USER) - - reverse_fail_list = [] - - # install newly packages - for path in Dir.glob("*_*_#{os}.zip") - # install - pkg_cl.install_local_pkg( path, false ) - end - - # get reverse-dependent source-codes - pkginfo = PackageManifest.new("#{parent_path}/package/pkginfo.manifest") - pkg_list = [] - for pkg in pkginfo.packages - pkg_list = pkg_list + pkg_cl.get_reverse_build_dependent_packages(pkg.package_name, os) - @log.info( "Extract reverse build dependency #{pkg.package_name} ...", Log::LV_USER) - end - pkg_list -= ignore_rev_dep_build_list - pkg_list.uniq! - - # download sources - src_path_hash = {} - pkg_list.each do |pkg| - # download - src_path = pkg_cl.download_source(pkg, os) - @log.info( "Downloaded #{pkg} source package to #{src_path}", Log::LV_USER) - - if src_path_hash[src_path].nil? then - src_path_hash[src_path] = [pkg] - else - src_path_hash[src_path] += [pkg] - end - end - src_path_list = src_path_hash.keys - - # add jobs for building reverse-dependent - src_path_list.each do |path| - # extract source package to test path - @log.info( " * Extracting source ... #{path}", Log::LV_USER) - test_src_path = "#{parent_path}/tmp_build" - FileUtils.mkdir_p test_src_path - Utils.execute_shell("cd \"#{test_src_path}\";tar xf #{path}") - - # build - @log.info( " * Building source ... ", Log::LV_USER) - result = build_test_with_pkg_client( pkg_cl, test_src_path, os, parent_path ) - FileUtils.rm_rf test_src_path - if not result then - reverse_fail_list += src_path_hash[path] - @log.error( "Build \"#{src_path_hash[path].join(", ")}\" test failed", Log::LV_USER) - if immediately then - return reverse_fail_list - end - end - end + # execute build command + def execute_build_command( target, src_path, build_root_dir, os, version ) - return reverse_fail_list.uniq - end + # get category + os_category = Utils.get_os_category( os ) + # convert directory format when windows + if Utils.is_windows_like_os( @host_os ) then + src_path2 = Utils.get_unix_path( src_path ) + else + src_path2 = src_path + end - # build test - def build_test_with_pkg_client( pkg_cl, src_path, os, parent_path) + env_def = + "BUILD_TARGET_OS=#{os} \ + TARGET_OS=#{os} \ + TARGET_OS_CATEGORY=#{os_category} \ + SRCDIR=\"#{src_path2}\" \ + ROOTDIR=\"#{build_root_dir}\" \ + VERSION=\"#{version}\" " + + # check script file + script_file = "#{src_path}/package/build.#{@host_os}" + if not File.exist? script_file then + if Utils.is_linux_like_os( @host_os ) then + script_file = "#{src_path}/package/build.linux" + elsif Utils.is_windows_like_os( @host_os ) then + script_file = "#{src_path}/package/build.windows" + elsif Utils.is_macos_like_os( @host_os ) then + script_file = "#{src_path}/package/build.macos" + end + # check old script file + if not File.exist? script_file then + @log.error( "The script file not found!: \"package/build.#{@host_os}\"", Log::LV_USER) + return false + end + end - local_pkg_list = [] - local_pkg_list += Dir.entries(parent_path).select{|e| e =~ /\.zip$/}.map{|p| parent_path + "/" + p} + # read build script + # this will ignore last lines without block + contents = [] + File.open( script_file, "r" ) do |f| + lines = [] + f.each_line do |l| + lines.push l + if l.start_with? "}" then + contents = contents + lines + lines = [] + end + end + end - # create pkginfo - pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + # generate shell script + File.open( "#{src_path}/.build.sh", "w" ) do |f| + f.puts "#!/bin/sh -xe" + contents.each do |l| + f.puts l + end - # install build dependencies - package_overwrite_list = [] - pkginfo.get_build_dependencies(os,@host_os).each do |dep| - if dep.target_os_list.count != 0 then - dep_target_os = dep.target_os_list[0] + case target + when "clean" + f.puts " " + when "build" + f.puts " " + when "build_cache" + f.puts "CACHEDIR=${PKG_CACHE_DIR}/$(cache_key)" + when "save_cache" + f.puts "rm -rf ${PKG_CACHE_DIR}/*" + f.puts "CACHEDIR=${PKG_CACHE_DIR}/$(cache_key)" + f.puts "mkdir -p ${CACHEDIR}" + when "install" + f.puts " " else - dep_target_os = os + @log.warn( "Wrong build-target is used: \"#{target}\"", Log::LV_USER) + return false end - - # get local dependent package - pkgexp = Regexp.new("\/#{dep.package_name}_.*_#{dep_target_os}\.zip$") - package_overwrite_list += local_pkg_list.select{|l| l =~ pkgexp} - - pkg_cl.install(dep.package_name, dep_target_os, true, false) - end - - # overwrite local dependent packages - package_overwrite_list.each do |l| - @log.info( "Package overwrite ..#{l}", Log::LV_USER) - pkg_cl.install_local_pkg(l,false) - end - - # source download - pkginfo.get_source_dependencies(os,@host_os).each do |dep| - pkg_cl.download_dep_source(dep.package_name) + f.puts "#{target}" + f.puts "echo \"success\"" end + Utils.execute_shell_with_log( "chmod +x #{src_path}/.build.sh", @log ) + build_command = "cd \"#{src_path}\";" + env_def + "./.build.sh" - # execute build script - build_root_dir = pkg_cl.location - env_def = - "BUILD_TARGET_OS=#{os} \ - SRCDIR=\"#{src_path}\" \ - ROOTDIR=\"#{build_root_dir}\" " - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} build" + # execute script if not Utils.execute_shell_with_log( build_command, @log ) then - @log.error( "Failed on build script", Log::LV_USER) + @log.error( "Failed on build script: \"#{target}\"", Log::LV_USER) return false else + Utils.execute_shell_with_log( "rm -rf #{src_path}/.build.sh", @log ) return true end - end - # write pkginfo.manifest and install/remove script + # write pkginfo.manifest def write_pkginfo_files(pkginfo,os,src_path) - for pkg in pkginfo.packages + # get category + os_category = Utils.get_os_category( os ) + + pkginfo.packages.each do |pkg| # skip if not support the target os - if not pkg.os.include? os + if not pkg.os_list.include? os next end - # install script files - copy_post_install_script(pkg,os,src_path); - copy_post_remove_script(pkg,os,src_path); + # install/remove script files + if not copy_post_install_script(pkg,os,src_path) then return false end + if not copy_post_remove_script(pkg,os,src_path) then return false end # write manifest file install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" - + # if there is no intall directory, error if not File.exist? install_dir then - @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) - @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) - return false + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + return false + end end - - # write pkginfo.manifest + + # write pkginfo.manifest File.open("#{install_dir}/pkginfo.manifest", "w") do |f| - pkg.print_to_file_with_os( f, os ) + pkg.print_to_file( f ) end end @@ -494,74 +481,207 @@ class Builder # copy post-install script def copy_post_install_script(pkg,os,src_path) - + tar = nil + src = nil - if File.exist? "#{src_path}/package/#{pkg.package_name}.install.#{os}" - src = "#{src_path}/package/#{pkg.package_name}.install.#{os}" - else - src = nil + # get category + os_category_list = [] + pkg.os_list.each do |cos| + os_category_list.push Utils.get_os_category(cos) + end + + # check compatable os + (pkg.os_list + os_category_list).uniq.each do |cos| + if File.exist? "#{src_path}/package/#{pkg.package_name}.install.#{cos}" then + if src.nil? then + src = "#{src_path}/package/#{pkg.package_name}.install.#{cos}" + else + @log.error( "compatable package can have only one install script\n but you have another availabe install scripts", Log::LV_USER) + @log.error( " * package/#{File.basename src}", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.install.#{cos}", Log::LV_USER) + return false + end + end + end + + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" + + # if there is no intall directory, error + if not File.exist? install_dir then + os_category = Utils.get_os_category( os ) + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + return false + end end if not src.nil? then - if os == "linux" or os == "darwin" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/install.sh" - elsif os == "windows" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/install.BAT" + if Utils.is_unix_like_os( os ) then + tar = "#{install_dir}/install.sh" + elsif Utils.is_windows_like_os( os) then + tar = "#{install_dir}/install.BAT" else puts "Unknown OS: #{os} " - return + return true end FileUtils.cp(src, tar) - end - - return + end + + return true end # copy post-remove script def copy_post_remove_script(pkg,os,src_path) - + tar = nil + src = nil - if File.exist? "#{src_path}/package/#{pkg.package_name}.remove.#{os}" - src = "#{src_path}/package/#{pkg.package_name}.remove.#{os}" - else - src = nil + # get category + os_category_list = [] + pkg.os_list.each do |cos| + os_category_list.push Utils.get_os_category(cos) + end + + # check compatable os + (pkg.os_list + os_category_list).uniq.each do |cos| + if File.exist? "#{src_path}/package/#{pkg.package_name}.remove.#{cos}" then + if src.nil? then + src = "#{src_path}/package/#{pkg.package_name}.remove.#{cos}" + else + @log.error( "compatable package can have only one remove script but you have another availabe remove scripts", Log::LV_USER) + @log.error( " * package/#{File.basename src}", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.remove.#{cos}", Log::LV_USER) + return false + end + end + end + + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" + + # if there is no intall directory, error + if not File.exist? install_dir then + os_category = Utils.get_os_category( os ) + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + return false + end end if not src.nil? - puts "------> #{src}" - if os == "linux" or os == "darwin" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/remove.sh" - puts "-0--\==> #{tar}" - elsif os == "windows" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/remove.BAT" + if Utils.is_unix_like_os( os ) then + tar = "#{install_dir}/remove.sh" + elsif Utils.is_windows_like_os( os) then + tar = "#{install_dir}/remove.BAT" else puts "Unknown OS: #{os} " - return + return true end FileUtils.cp(src, tar) - end + end + return true end # create package file def make_zip(pkginfo,os,src_path) - for pkg in pkginfo.packages + # get category + os_category = Utils.get_os_category( os ) + + pkginfo.packages.each do |pkg| # skip if not support the target os - if not pkg.os.include? os + if not pkg.os_list.include? os next end # cd install dir install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" + if not File.exist? install_dir then + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + return false + end + end # zip @log.info( "Creating package file ... #{pkg.package_name}_#{pkg.version}_#{os}.zip", Log::LV_USER) - Utils.execute_shell("cd \"#{install_dir}\"; zip -r -y #{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip *") + @log.info("cd \"#{install_dir}\"; zip -r -y #{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip *") + Utils.execute_shell_with_log("cd \"#{install_dir}\"; zip -r -y #{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip *", @log) + if not File.exist? "#{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip" then + return false + end + end + return true + end + + + # clean the temporary directory for packaged + def clean_project_directory(src_path, target_os = nil) + + # if os is not set, use host os instead + if target_os.nil? then target_os = @host_os end + + # convert path if windows + if Utils.is_windows_like_os(@host_os) then + build_root_dir = Utils.get_unix_path( @buildroot_dir ) + else + build_root_dir = @buildroot_dir + end + + # create pkginfo + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + return false end + + # get category + # make clean + pkginfo.packages.each do |pkg| + os = pkg.os + os_category = Utils.get_os_category( os ) + + if File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" + elsif File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + end + end + + # clean local-only package's directory + if File.exist? "#{src_path}/package/pkginfo.manifest.local" then + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest.local") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end + pkginfo.packages.each do |pkg| + os = pkg.os + os_category = Utils.get_os_category( os ) + + if File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" + elsif File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + end + end + end + + # execute + return execute_build_command("clean", src_path, build_root_dir, target_os, pkginfo.get_version) end end diff --git a/src/builder/CleanOptionParser.rb b/src/builder/CleanOptionParser.rb index 31611fc..b2f5ad8 100644 --- a/src/builder/CleanOptionParser.rb +++ b/src/builder/CleanOptionParser.rb @@ -28,17 +28,27 @@ Contributors: require 'optparse' require 'logger' +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "utils" def parse() #option parsing option = {} - optparse = OptionParser.new do |opts| - opts.banner = "Usage: pkg-clean" - opts.on('-h','--help', 'display this information') do + optparse = OptionParser.new(nil, 32, ' '*8) do |opts| + opts.banner = "Clean the package service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-clean [-h] [-v]" + "\n" \ + + "\n" + "Options:" + "\n" + + opts.on('-h','--help', 'display help') do puts opts exit end + + opts.on('-v','--version', 'display version') do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end end optparse.parse! diff --git a/src/builder/optionparser.rb b/src/builder/optionparser.rb index 05888f0..3f23fc3 100644 --- a/src/builder/optionparser.rb +++ b/src/builder/optionparser.rb @@ -28,35 +28,53 @@ Contributors: require 'optparse' require 'logger' +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "utils" def parse() #option parsing option = {} optparse = OptionParser.new do |opts| - opts.banner = "Usage: pkg-build -u -o -c -h" - opts.on('-u','--url ', 'remote package server url') do |url| + opts.banner = "Build and packaging service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-build -u [-o ] [-c] [-h] [-v]" + "\n" \ + + "\n" + "Options:" + "\n" + + opts.on('-u','--url ', 'remote package server url: http://127.0.0.1/dibs/unstable') do |url| option[:url] = url end + option[:os] = nil opts.on('-o','--os ', 'operating system ') do |os| option[:os] = os - end + end + option[:clean] = false opts.on('-c','--clean', 'clean build') do option[:clean] = true - end + end + option[:rev] = false - opts.on('-r','--rev', 'reverse build dependency check') do - option[:rev] = true - end - opts.on('-h','--help', 'display this information') do + #opts.on('-r','--rev', 'reverse build dependency check') do + # option[:rev] = true + #end + + opts.on('-h','--help', 'display help') do puts opts exit + end + + opts.on('-v','--version', 'display version') do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit end - end + end optparse.parse! + + if option[:url].nil? or option[:url].empty? then + raise ArgumentError, "Usage: pkg-build -u [-o ] [-c] [-h]" + end - return option + return option end diff --git a/src/common/Action.rb b/src/common/Action.rb new file mode 100644 index 0000000..7b64559 --- /dev/null +++ b/src/common/Action.rb @@ -0,0 +1,47 @@ +=begin + + Action.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class Action + attr_accessor :time, :period + + def initialize( time, period ) + @time = time + @period = period + end + + + # initialize action + def init() + end + + + # execute action + def execute() + end + +end diff --git a/src/common/PackageManifest.rb b/src/common/PackageManifest.rb index 4a9c2d3..044b7b6 100644 --- a/src/common/PackageManifest.rb +++ b/src/common/PackageManifest.rb @@ -33,30 +33,23 @@ class PackageManifest attr_accessor :packages def initialize( file_path ) - @pkg_map = Parser.read_pkginfo_list( file_path ) - @packages = @pkg_map.values + @packages = Parser.read_multy_pkginfo_from file_path end # scan all build dependencies - def get_build_dependencies( target_os, host_os ) + def get_build_dependencies( target_os ) # for all list = [] - for pkg in @packages + @packages.each do |pkg| # package that has the target os - if not pkg.os.include?(target_os) + if not pkg.os_list.include?(target_os) next end - # package that has the host os - if not pkg.build_host_os.include?(host_os) - next - end # package that has the target os - for dep in pkg.build_dep_list - # if dep.target_os_list.include? target_os - list.push dep - # end + pkg.build_dep_list.each do |dep| + list.push dep end end list.uniq! @@ -65,13 +58,13 @@ class PackageManifest end - # scan all build dependencies + # scan all source dependencies def get_source_dependencies( target_os, host_os ) # for all list = [] - for pkg in @packages + @packages.each do |pkg| # only package that used in target os - if not pkg.os.include?(target_os) + if not pkg.os_list.include?(target_os) next end @@ -81,7 +74,7 @@ class PackageManifest end # package that has the target os - for dep in pkg.source_dep_list + pkg.source_dep_list.each do |dep| # if dep.target_os_list.include? target_os list.push dep # end @@ -93,10 +86,32 @@ class PackageManifest end + # scan all install dependencies + def get_install_dependencies( target_os, pkg_name=nil ) + # for all + list = [] + @packages.each do |pkg| + if not pkg_name.nil? and pkg.package_name != pkg_name then next end + # only package that used in target os + if not pkg.os_list.include?(target_os) + next + end + + # package that has the target os + pkg.install_dep_list.each do |dep| + list.push dep + end + end + list.uniq! + + return list + end + + def package_exist?(target_os, host_os) - for pkg in @packages + @packages.each do |pkg| # only package that used in target os - if pkg.os.include?(target_os) and + if pkg.os_list.include?(target_os) and pkg.build_host_os.include?(host_os) return true end @@ -104,4 +119,34 @@ class PackageManifest return false end + + + def get_version() + return @packages[0].version + end + + + def get_target_packages(target_os) + pkgs = [] + @packages.each do |pkg| + if pkg.os_list.include?(target_os) then + pkgs.push pkg + end + end + + return pkgs + end + + + def pkg_exist?(name,ver,os) + @packages.each do |pkg| + if pkg.package_name != name then next end + if not ver.nil? and pkg.version != ver then next end + if not os.nil? and not pkg.os_list.include?(os) then next end + + return true + end + + return false + end end diff --git a/src/common/ScheduledActionHandler.rb b/src/common/ScheduledActionHandler.rb new file mode 100644 index 0000000..a62c5f0 --- /dev/null +++ b/src/common/ScheduledActionHandler.rb @@ -0,0 +1,99 @@ +=begin + + ScheduledActionHandler.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +$LOAD_PATH.unshift File.dirname(__FILE__) + +class ScheduledActionHandler + attr_accessor :quit + + # init + def initialize( ) + @thread = nil + @quit = false + @actions = [] + end + + + # register a action + def register( action ) + # init action + action.init + # add to list + @actions.push action + end + + + # start thread + def start() + @thread = Thread.new { + # main + thread_main() + + # close + terminate() + } + end + + + protected + + def thread_main + + while not @quit + + current_time = Time.new + + # get list + action_list = Array.new(@actions) + action_list.each do |action| + # if its time is reached, execute action + if not action.time.nil? and current_time > action.time then + action.execute + + # if periodic action, renew the time + # else remove it from list + if action.period != 0 then + while current_time > action.time + action.time = action.time + action.period + end + else + @actions.delete(action) + end + end + end + + # sleep 10 sec + sleep 10 + end + end + + + def terminate + end + +end diff --git a/src/common/Version.rb b/src/common/Version.rb index 3bdba12..633d378 100644 --- a/src/common/Version.rb +++ b/src/common/Version.rb @@ -45,5 +45,10 @@ class Version < Array end def == x (self <=> x) == 0 + end + def compare x + if self < x then return -1 + elsif self == x then return 0 + else return 1 end end end diff --git a/src/common/fileTransfer.rb b/src/common/fileTransfer.rb new file mode 100644 index 0000000..cda6a4b --- /dev/null +++ b/src/common/fileTransfer.rb @@ -0,0 +1,118 @@ + +require 'socket' + +class FileTransfer + + def FileTransfer.putfile(ip, port, username, passwd, bpath, logger) + filename = File.basename(bpath) + uniqdir = Utils.create_uniq_name + ftp_filepath = File.join(uniqdir, filename) + + begin + ftp = Net::FTP.new + if port.nil? or port == "" then + ftp.connect(ip) + else + ftp.connect(ip, port) + end + logger.info "[FTP log] Connected FTP server (#{ip}:#{port})" + ftp.login(username, passwd) + ftp.binary = true + ftp.passive = true + ftp.mkdir(uniqdir) + ftp.chdir(uniqdir) + ftp.put(bpath) + logger.info "[FTP log] Put a file" + logger.info "[FTP log] from \"#{bpath}\" to \"#{ftp_filepath}\"" + files = ftp.list(filename) + if files.empty? then + logger.error "[FTP log] Failed to upload file (#{filename} does not exist)" + return nil + end + ftp.quit + logger.info "[FTP log] Disconnected FTP server" + rescue => e + logger.error "[FTP log] Exception" + logger.error e.message + logger.error e.backtrace.inspect + return nil + end + return ftp_filepath + end + + def FileTransfer.getfile(ip, port, username, passwd, bpath, target, logger) + dirname = File.dirname(bpath) + filename = File.basename(bpath) + + # target can be directory or file + if File.directory? target then + dst_file = File.join(target,filename) + else + dst_file = target + end + + begin + ftp = Net::FTP.new + if port.nil? or port == "" then + ftp.connect(ip) + else + ftp.connect(ip, port) + end + logger.info "[FTP log] Connected FTP server (#{ip}:#{port})" + ftp.login(username, passwd) + ftp.binary = true + ftp.passive = true + ftp.chdir(dirname) + ftp.get(filename, dst_file) + logger.info "[FTP log] Get a file" + logger.info "[FTP log] from \"#{bpath}\" to \"#{dst_file}\"" + ftp.quit + logger.info "[FTP log] Disconnected FTP server" + rescue => e + logger.error "[FTP log] Exception" + logger.error e.message + logger.error e.backtrace.inspect + return nil + end + if not File.exist? dst_file then + logger.error "[FTP log] Failed to download file (#{dst_file} does not exist)" + return nil + end + return bpath + end + + def FileTransfer.cleandir(ip, port, username, passwd, path, logger) + dirname = File.dirname(path) + + begin + ftp = Net::FTP.new + if port.nil? or port == "" then + ftp.connect(ip) + else + ftp.connect(ip, port) + end + logger.info "[FTP log] Connected FTP server (#{ip}:#{port})" + ftp.login(username, passwd) + old_dir = ftp.pwd + ftp.chdir(dirname) + list = ftp.ls + # TODO: if list is directory? + list.each do |l| + file = l.split(" ")[-1].strip + ftp.delete(file) + end + ftp.chdir(old_dir) + ftp.rmdir(dirname) + logger.info "[FTP log] Clean dir (#{dirname})" + ftp.quit + logger.info "[FTP log] Disconnected FTP server" + rescue => e + logger.error "[FTP log] Exception" + logger.error e.message + logger.error e.backtrace.inspect + return nil + end + + return true + end +end diff --git a/src/common/log.rb b/src/common/log.rb index afa8fd7..e7eb190 100644 --- a/src/common/log.rb +++ b/src/common/log.rb @@ -30,7 +30,7 @@ require "logger" class Log - attr_accessor :path + attr_accessor :path, :cnt # Log LEVEL LV_NORMAL = 1 @@ -39,6 +39,7 @@ class Log # init def initialize(path, lv=LV_USER) + @cnt = 0 @path = path if @path.nil? then @logger = Logger.new(STDOUT) @@ -52,43 +53,47 @@ class Log # diable logger format @default_formatter = @logger.formatter @no_prefix_formatter = proc do |severity, datetime, progname, msg| - " >#{msg}" + " >#{msg}" end end def info(msg, lv=LV_NORMAL) - if @path.nil? then puts "Info: #{msg}" + if @path.nil? and not @second_out.nil? then puts "Info: #{msg}" else @logger.info msg end - if not @second_out.nil? and lv >= @second_out_level then - output_extra "Info: " + msg + if not @second_out.nil? and lv >= @second_out_level then + output_extra "Info: " + msg end - end + @cnt = @cnt + 1 + end - def warn(msg, lv=LV_NORMAL) - if @path.nil? then puts "Warn: #{msg}" + def warn(msg, lv=LV_NORMAL) + if @path.nil? and not @second_out.nil? then puts "Warn: #{msg}" else @logger.warn msg end if not @second_out.nil? and lv >= @second_out_level then - output_extra "Warn: " + msg + output_extra "Warn: " + msg end - end + @cnt = @cnt + 1 + end - def error(msg, lv=LV_NORMAL) - if @path.nil? then puts "Error: #{msg}" + def error(msg, lv=LV_NORMAL) + if @path.nil? and not @second_out.nil? then puts "Error: #{msg}" else @logger.error msg end if not @second_out.nil? and lv >= @second_out_level then - output_extra "Error: " + msg + output_extra "Error: " + msg end + @cnt = @cnt + 1 end - def output(msg, lv=LV_NORMAL) - if @path.nil? then puts msg + def output(msg, lv=LV_NORMAL) + if @path.nil? and not @second_out.nil? then puts msg else @logger.info msg end if not @second_out.nil? and lv >= @second_out_level then - output_extra msg + output_extra msg end + @cnt = @cnt + 1 end @@ -99,7 +104,6 @@ class Log protected def output_extra(msg) - #do nothing - end - + #do nothing + end end diff --git a/src/common/package.rb b/src/common/package.rb index 122f977..067342a 100644 --- a/src/common/package.rb +++ b/src/common/package.rb @@ -1,6 +1,6 @@ =begin - - package.rb + + package.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -27,11 +27,13 @@ Contributors: =end class Package - attr_accessor :package_name, :version, :os, :build_host_os, :maintainer, :attribute, :install_dep_list, :build_dep_list, :source_dep_list, :conflicts, :source, :src_path, :path, :origin, :checksum, :size, :description + attr_accessor :package_name, :label, :version, :os, :build_host_os, :maintainer, :attribute, :install_dep_list, :build_dep_list, :source_dep_list, :conflicts, :source, :src_path, :path, :origin, :checksum, :size, :description, :os_list, :custom def initialize (package_name) @package_name = package_name + @label = "" @version = "" @os = "" + @os_list = [] @build_host_os = [] @maintainer = "" @attribute = [] @@ -46,116 +48,45 @@ class Package @checksum = "" @size = "" @description = "" - end + @custom = "" + end + def print - puts "Package : " + @package_name - if not @version.empty? then puts "Version : " + @version end - if not @os.empty? then puts "OS : " + @os end - if not @build_host_os.empty? then puts "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then puts "Maintainer : " + @maintainer end - if not @attribute.empty? then puts "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - puts "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - puts "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - puts "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - puts "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then puts "Source : " + @source end - if not @src_path.empty? then puts "Src-path : " + @src_path end - if not @path.empty? then puts "Path : " + @path end - if not @origin.empty? then puts "Origin : " + @origin end - if not @checksum.empty? then puts "SHA256 : " + @checksum end - if not @size.empty? then puts "Size : " + @size end - if not @description.empty? then puts "Description : " + @description end - end + puts self.to_s + end def to_s string = "Package : " + @package_name - if not @version.empty? then string = string + "\n" + "Version : " + @version end - if not @os.empty? then string = string + "\n" + "OS : " + @os end - if not @build_host_os.empty? then string = string + "\n" + "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then string = string + "\n" + "Maintainer : " + @maintainer end - if not @attribute.empty? then string = string + "\n" + "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then + if not @label.empty? then string = string + "\n" + "Label : " + @label end + if not @version.empty? then string = string + "\n" + "Version : " + @version end + if not @os_list.empty? then string = string + "\n" + "OS : " + @os_list.join(", ") end + if not @build_host_os.empty? then string = string + "\n" + "Build-host-os : " + @build_host_os.join(", ") end + if not @maintainer.empty? then string = string + "\n" + "Maintainer : " + @maintainer end + if not @attribute.empty? then string = string + "\n" + "Attribute : " + @attribute.join("|") end + if not @install_dep_list.empty? then string = string + "\n" + "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then + end + if not @build_dep_list.empty? then string = string + "\n" + "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then + end + if not @source_dep_list.empty? then string = string + "\n" + "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then + end + if not @conflicts.empty? then string = string + "\n" + "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then string = string + "\n" + "Source : " + @source end - if not @src_path.empty? then string = string + "\n" + "Src-path : " + @src_path end - if not @path.empty? then string = string + "\n" + "Path : " + @path end - if not @origin.empty? then string = string + "\n" + "Origin : " + @origin end - if not @checksum.empty? then string = string + "\n" + "SHA256 : " + @checksum end - if not @size.empty? then string = string + "\n" + "Size : " + @size end - if not @description.empty? then string = string + "\n" + "Description : " + @description end - return string - end - def print_to_file(file) - file.puts "Package : " + @package_name - if not @version.empty? then file.puts "Version : " + @version end - if not @os.empty? then file.puts "OS : " + @os end - if not @build_host_os.empty? then file.puts "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then file.puts "Maintainer : " + @maintainer end - if not @attribute.empty? then file.puts "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - file.puts "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - file.puts "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - file.puts "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - file.puts "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then file.puts "Source : " + @source end - if not @src_path.empty? then file.puts "Src-path : " + @src_path end - if not @path.empty? then file.puts "Path : " + @path end - if not @origin.empty? then file.puts "Origin : " + @origin end - if not @checksum.empty? then file.puts "SHA256 : " + @checksum end - if not @size.empty? then file.puts "Size : " + @size end - if not @description.empty? then file.puts "Description : " + @description end + end + if not @source.empty? then string = string + "\n" + "Source : " + @source end + if not @src_path.empty? then string = string + "\n" + "Src-path : " + @src_path end + if not @path.empty? then string = string + "\n" + "Path : " + @path end + if not @origin.empty? then string = string + "\n" + "Origin : " + @origin end + if not @checksum.empty? then string = string + "\n" + "SHA256 : " + @checksum end + if not @size.empty? then string = string + "\n" + "Size : " + @size end + if not @custom.empty? then string = string + "\n" + @custom end + if not @description.empty? then string = string + "\n" + "Description : " + @description end + return string end - def print_to_file_with_os(file,target_os) - file.puts "Package : " + @package_name - if not @version.empty? then file.puts "Version : " + @version end - file.puts "OS : " + target_os - if not @build_host_os.empty? then file.puts "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then file.puts "Maintainer : " + @maintainer end - if not @attribute.empty? then file.puts "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - file.puts "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - file.puts "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - file.puts "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - file.puts "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then file.puts "Source : " + @source end - if not @src_path.empty? then file.puts "Src-path : " + @src_path end - if not @path.empty? then file.puts "Path : " + @path end - if not @origin.empty? then file.puts "Origin : " + @origin end - if not @checksum.empty? then file.puts "SHA256 : " + @checksum end - if not @size.empty? then file.puts "Size : " + @size end - if not @description.empty? then file.puts "Description : " + @description end + def print_to_file(file) + file.puts self.to_s end -end +end diff --git a/src/common/parser.rb b/src/common/parser.rb index 01f7acf..8bb863d 100644 --- a/src/common/parser.rb +++ b/src/common/parser.rb @@ -1,6 +1,6 @@ =begin - - parser.rb + + parser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -31,172 +31,304 @@ require "package" require "dependency" class Parser - def Parser.read_pkginfo_list (file) - pkglist = {} + def Parser.read_multy_pkginfo_from (file, only_common = false) + pkglist = [] + package = nil + + #file check + File.open file,"r" do |f| #variable initialize - package_name = "" - version = "" - os = "" - build_host_os = [] - maintainer = "" - attribute = [] - install_dep_list = [] - build_dep_list = [] - source_dep_list = [] - conflicts = [] - source = "" - src_path = "" - path = "" - origin = "" - checksum = "" - size = "" - description = "" - + state = "INIT" + common_source = "" + common_version = "" + common_maintainer = "" f.each_line do |l| # separator - if l.strip.empty? then - #make package and initialize - if not package_name.empty? and not os.empty? then - package = Package.new(package_name) - if not version.empty? then package.version = version end - if not os.empty? then package.os = os end - if not build_host_os.empty? then package.build_host_os = build_host_os end - if not maintainer.empty? then package.maintainer = maintainer end - if not attribute.empty? then package.attribute = attribute end - if not install_dep_list.empty? then package.install_dep_list = install_dep_list end - if not build_dep_list.empty? then package.build_dep_list = build_dep_list end - if not source_dep_list.empty? then package.source_dep_list = source_dep_list end - if not conflicts.empty? then package.conflicts = conflicts end - if not source.empty? then package.source = source end - if not src_path.empty? then package.src_path = src_path end - if not path.empty? then package.path = path end - if not origin.empty? then package.origin = origin end - if not checksum.empty? then package.checksum = checksum end - if not size.empty? then package.size = size end - if not description.empty? then package.description = description end - pkglist[[package_name,os]] = package - package_name = "" - version = "" - os = "" - bulid_host_os = [] - maintainer = "" - attribute = [] - install_dep_list = [] - build_dep_list = [] - source_dep_list = [] - conflicts = [] - source = "" - src_path = "" - path = "" - origin = "" - checksum = "" - size = "" - description = "" - end - next - end - # commant - if l.strip.start_with? "#" then next end - #contents - dsic_on = false - case l.strip.split(':')[0].strip - when /^Package/i then - package_name = l.sub(/^[ \t]*Package[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Version/i then - version = l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^OS/i then - os = l.sub(/^[ \t]*OS[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Build-host-os/i then - build_host_os = l.sub(/^[ \t]*Build-host-os[ \t]*:[ \t]*/i,"").tr(" \t\n\r", "").split("|") - disc_on=false - when /^Maintainer/i then - maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Attribute/i then - attribute = l.sub(/^[ \t]*Attribute[ \t]*:[ \t]*/i,"").tr(" \t\n\r","").split("|") - disc_on=false - when /^Install-dependency/i then - install_dep_list = dep_parser l.sub(/^[ \t]*Install-dependency[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Build-dependency/i then - build_dep_list = dep_parser l.sub(/^[ \t]*Build-dependency[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Source-dependency/i then - source_dep_list = dep_parser l.sub(/^[ \t]*Source-dependency[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Conflicts/i then - conflicts = dep_parser l.sub(/^[ \t]*Conflicts[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Source/i then - source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Src-path/i then - src_path = l.sub(/^[ \t]*Src-path[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Path/i then - path = l.sub(/^[ \t]*Path[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Origin/i then - origin = l.sub(/^[ \t]*Origin[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^SHA256/i then - checksum = l.sub(/^[ \t]*SHA256[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Size/i then - size = l.sub(/^[ \t]*Size[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Description/i then - description = l.sub(/^[ \t]*Description[ \t]*:[ \t]*/i,"") - disc_on=true - else - if disc_on then - description = description + l - else - puts "unknown section : #{l}" - end - end + if l.strip.empty? then + #make package and initialize + if state == "PACKAGE" then + if not package.package_name.empty? then + pkglist.push package + else + raise RuntimeError, "#{file} format is not valid" + end + end + state = "INIT" + package = nil + next + end + # commant + if l.strip.start_with? "#" then next end + #contents + dsic_on = false + case l.strip.split(':')[0].strip + when /^Package/i then + if only_common then return [common_source, common_version, common_maintainer] end + # state control + case state + when "INIT" then state = "PACKAGE" + when "COMMON" then state = "PACKAGE" + when "PACKAGE" then + if not package.package_name.empty? then + pkglist.push package + else + raise RuntimeError, "Package name is not set in \"#{file}\" file" + end + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end - end - #i essent - - # check last package - if not package_name.empty? and not os.empty? then - package = Package.new(package_name) - if not version.empty? then package.version = version end - if not os.empty? then package.os = os end - if not build_host_os.empty? then package.build_host_os = build_host_os end - if not maintainer.empty? then package.maintainer = maintainer end - if not attribute.empty? then package.attribute = attribute end - if not install_dep_list.empty? then package.install_dep_list = install_dep_list end - if not build_dep_list.empty? then package.build_dep_list = build_dep_list end - if not source_dep_list.empty? then package.source_dep_list = source_dep_list end - if not conflicts.empty? then package.conflicts = conflicts end - if not source.empty? then package.source = source end - if not src_path.empty? then package.src_path = src_path end - if not path.empty? then package.path = path end - if not origin.empty? then package.origin = origin end - if not checksum.empty? then package.checksum = checksum end - if not size.empty? then package.size = size end - if not description.empty? then package.description = description end - pkglist[[package_name,os]] = package - end - end - return pkglist - end + package_name = l.sub(/^[ \t]*Package[ \t]*:[ \t]*/i,"").strip + if not package_name.empty? then + package = Package.new(package_name) + package.source = common_source + package.version = common_version + package.maintainer = common_maintainer + else + raise RuntimeError, "Package name is not set in \"#{file}\" file" + end + disc_on=false + when /^Label/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Label field in Common section in \"#{file}\" file" + when "PACKAGE" then package.label = l.sub(/^[ \t]*Label[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Version/i then + case state + when "INIT" , "COMMON" then + if common_version.empty? then + common_version = l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip + else + raise RuntimeError, "Version information is conflict in \"#{file}\" file\nIf use Version field in Common section then Package section can't contain Version field" + end + when "PACKAGE" then + if common_version.empty? then + package.version = l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip + else + raise RuntimeError, "Version information is conflict in \"#{file}\" file\nIf use Version field in Common section then Package section can't contain Version field" + end + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^OS/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support OS field in Common section in \"#{file}\" file" + when "PACKAGE" then + package.os_list = l.sub(/^[ \t]*OS[ \t]*:[ \t]*/i,"").tr(" \t\n\r", "").split(",") + package.os = package.os_list[0] + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Build-host-os/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Build-host-os field in Common section in \"#{file}\" file" + when "PACKAGE" then package.build_host_os = l.sub(/^[ \t]*Build-host-os[ \t]*:[ \t]*/i,"").tr(" \t\n\r", "").split(",") + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Maintainer/i then + case state + when "INIT" , "COMMON" then + if common_maintainer.empty? then + common_maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip + else + raise RuntimeError, "Maintainer information is conflict in \"#{file}\" file\nIf use Maintainer field in Common section then Package section can't contain Maintainer field" + end + when "PACKAGE" then + if common_maintainer.empty? then + package.maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip + else + raise RuntimeError, "Maintainer information is conflict in \"#{file}\" file\nIf use Maintainer field in Common section then Package section can't contain Maintainer field" + end + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Attribute/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Attribute field in Common section in \"#{file}\" file" + when "PACKAGE" then package.attribute = l.sub(/^[ \t]*Attribute[ \t]*:[ \t]*/i,"").tr(" \t\n\r","").split("|") + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Install-dependency/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Install-dependency field in Common section in \"#{file}\" file" + when "PACKAGE" then package.install_dep_list = dep_parser l.sub(/^[ \t]*Install-dependency[ \t]*:[ \t]*/i,"").split(',') + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Build-dependency/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Build-dependency field in Common section in \"#{file}\" file" + when "PACKAGE" then package.build_dep_list = dep_parser l.sub(/^[ \t]*Build-dependency[ \t]*:[ \t]*/i,"").split(',') + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Source-dependency/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Source-dependency field in Common section in \"#{file}\" file" + when "PACKAGE" then package.source_dep_list = dep_parser l.sub(/^[ \t]*Source-dependency[ \t]*:[ \t]*/i,"").split(',') + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Conflicts/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Conflicts field in Common section in \"#{file}\" file" + when "PACKAGE" then package.conflicts = dep_parser l.sub(/^[ \t]*Conflicts[ \t]*:[ \t]*/i,"").split(',') + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Source/i then + case state + when "INIT" , "COMMON" then + state = "COMMON" + if common_source.empty? then + common_source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip + else + raise RuntimeError, "Source information is conflict in \"#{file}\" file\nIf use Source field in Common section then Package section can't contain Source field" + end + when "PACKAGE" then + if common_source.empty? then + package.source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip + else + raise RuntimeError, "Source information is conflict in \"#{file}\" file\nIf use Source field in Common section then Package section can't contain Source field" + end + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Src-path/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Src-path field in Common section in \"#{file}\" file" + when "PACKAGE" then + package.src_path = l.sub(/^[ \t]*Src-path[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^ORIGIN/ then + #for compatable + next + when /^Include/i then + case state + when "INIT", "COMMON" then + pfile = File.dirname(file) + "/" + l.sub(/^[ \t]*Include[ \t]*:[ \t]*/i,"").strip + if File.exist? pfile then + pkglist = Parser.read_multy_pkginfo_from pfile + list = Parser.read_multy_pkginfo_from(pfile, true) + common_source = list[0] + common_version = list[1] + common_maintainer = list[2] + else + raise RuntimeError, "Not exist \"#{pfile}\"" + end + when "PACKAGE" then raise RuntimeError, "Not support Include field in Common section in \"#{file}\" file" + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Path/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Path field in Common section in \"#{file}\" file" + when "PACKAGE" then package.path = l.sub(/^[ \t]*Path[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Origin/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Origin field in Common section in \"#{file}\" file" + when "PACKAGE" then package.origin = l.sub(/^[ \t]*Origin[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^SHA256/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support SHA256 field in Common section in \"#{file}\" file" + when "PACKAGE" then package.checksum = l.sub(/^[ \t]*SHA256[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Size/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Size field in Common section in \"#{file}\" file" + when "PACKAGE" then package.size = l.sub(/^[ \t]*Size[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + when /^Description/i then + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Description field in Common section in \"#{file}\" file" + when "PACKAGE" then package.description = l.sub(/^[ \t]*Description[ \t]*:[ \t]*/i,"") + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=true + when /^C-/ then + #custom field + case state + when "INIT" then raise RuntimeError, "\"Package :\" string must be infront of Package section in \"#{file}\" file" + when "COMMON" then raise RuntimeError, "Not support Description field in Common section in \"#{file}\" file" + when "PACKAGE" then + if package.custom.empty? then + package.custom = l.strip + else + package.custom = package.custom + "\n" + l.strip + end + else raise RuntimeError, "UNKNOWN parser state : #{state}" + end + disc_on=false + else + if disc_on and state == "PACKAGE" then + package.description = package.description + l + else + raise RuntimeError, "Can't parse below line in \"#{file}\" file \n\t#{l}" + end + end + end - def Parser.read_pkginfo (file) - return read_pkg_list(file).values[0] - end + if only_common then return [common_source, common_version, common_maintainer] end + + # check last package + if state == "PACKAGE" then + if not package.package_name.empty? then + pkglist.push package + else + raise RuntimeError, "Package name is not set in \"#{file}\" file" + end + end + end + return pkglist + end + + def Parser.read_single_pkginfo_from (file) + return read_multy_pkginfo_from(file)[0] + end - def Parser.read_pkg_list (file) + def Parser.read_repo_pkg_list_from (file) result = {} - read_pkginfo_list(file).values.each { |x| result[x.package_name]=x } + read_multy_pkginfo_from(file).each { |x| result[x.package_name]=x } return result - end + end + + #for test + def Parser.print (array) + array.each do |package| + puts package.to_s + puts "" + end + end private def Parser.dep_parser (string_list) @@ -208,7 +340,7 @@ class Parser base_version = nil target_os_list = [] #string trim - dependency = dep.tr " \t\n", "" + dependency = dep.tr " \t\r\n", "" #version extract vs = dependency.index('(') ve = dependency.index(')') diff --git a/src/common/utils.rb b/src/common/utils.rb index 24b12ff..ea2bf95 100644 --- a/src/common/utils.rb +++ b/src/common/utils.rb @@ -27,32 +27,61 @@ Contributors: =end class Utils + STARTUP_INFO_SIZE = 68 + PROCESS_INFO_SIZE = 16 + NORMAL_PRIORITY_CLASS = 0x00000020 + + def Utils.identify_current_OS() + os = "UnsupportedOS" - if defined?(HOST_OS).nil? then case `uname -s`.strip when "Linux" - HOST_OS = "linux" - when /MINGW32.*/ - HOST_OS = "windows" + if File.exist? "/etc/debian_version" then + arch = (`uname -i`.strip == "x86_64") ? "64" : "32" + os = "ubuntu-#{arch}" + elsif File.exist? "/etc/redhat-release" then + os = "redhat-unknown" + elsif File.exist? "/etc/SuSE-release" then + arch = (`uname -i`.strip == "x86_64") ? "64" : "32" + os = "opensuse-#{arch}" + elsif File.exist? "/etc/mandrake-release" then + os = "mandrake-unknown" + end + when "MINGW32_NT-5.1" + progfile_path = Utils.execute_shell_return("echo $PROGRAMFILES","windows")[0].strip + if progfile_path.include?("(x86)") then arch = "64" else arch = "32" end + os = "windows-#{arch}" + when "MINGW32_NT-6.1" + progfile_path = Utils.execute_shell_return("echo $PROGRAMFILES","windows")[0].strip + if progfile_path.include?("(x86)") then arch = "64" else arch = "32" end + os = "windows-#{arch}" when "Darwin" - HOST_OS = "darwin" - else - end + os = "macos-64" + end + + return os end - # set static variable in WORKING_DIR, HOME - if defined?(WORKING_DIR).nil? then WORKING_DIR = Dir.pwd end - if defined?(HOME).nil? then - # get home directory, using Dir.chdir - Dir.chdir - HOME = Dir.pwd - Dir.chdir WORKING_DIR + + def Utils.check_host_OS() + if Utils.get_all_OSs().include? HOST_OS then + return true + else + return false + end + end + + + def Utils.get_all_OSs() + return ["ubuntu-32","ubuntu-64","windows-32","windows-64","macos-64","opensuse-32", "opensuse-64"] end + def Utils.create_uniq_name time = Time.new + # uniq snapshot_name name is year_month_day_hour_min_sec_microsec - return time.strftime("%m%d%H%M%S") + time.usec.to_s() + return time.strftime("%m%d%H%M%S") + time.usec.to_s.rjust(6, '0') end def Utils.is_url_remote(url) @@ -109,26 +138,44 @@ class Utils return 0 end + def Utils.execute_shell_generate(cmd, os_category = nil) + result_lines = [] + + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then + mingw_path = "sh.exe -c " + cmd = cmd.gsub("\"", "\\\"") + cmd = mingw_path + "\"#{cmd}\"" + end - def Utils.execute_shell(cmd) + return cmd + end + + + def Utils.execute_shell(cmd, os_category = nil) ret = false - if HOST_OS.eql? "windows" then + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then mingw_path = "sh.exe -c " cmd = cmd.gsub("\"", "\\\"") cmd = mingw_path + "\"#{cmd}\"" end - system "#{cmd}" + `#{cmd}` if $?.to_i == 0 then ret = true else ret = false end return ret end - def Utils.execute_shell_return(cmd) + def Utils.execute_shell_return(cmd, os_category = nil) result_lines = [] - if HOST_OS.eql? "windows" then + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then mingw_path = "sh.exe -c " cmd = cmd.gsub("\"", "\\\"") cmd = mingw_path + "\"#{cmd}\"" @@ -148,8 +195,10 @@ class Utils end end - def Utils.execute_shell_return_ret(cmd) - if HOST_OS.eql? "windows" then + def Utils.execute_shell_return_ret(cmd, os_category = nil) + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then mingw_path = "sh.exe -c " cmd = cmd.gsub("\"", "\\\"") cmd = mingw_path + "\"#{cmd}\"" @@ -158,9 +207,11 @@ class Utils return `#{cmd}` end - def Utils.execute_shell_with_log(cmd, log) + def Utils.execute_shell_with_log(cmd, log, os_category = nil) - if HOST_OS.eql? "windows" then + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then mingw_path = "sh.exe -c " cmd = cmd.gsub("\"", "\\\"") cmd = mingw_path + "\"#{cmd}\"" @@ -180,16 +231,79 @@ class Utils end end + def Utils.spawn(cmd, os_category = nil) + + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then + create_process(cmd) + else + fork do + exec(cmd) + end + end + + end + + def Utils.create_process(command,redirStdout="", redirStderr="") + + if redirStdout.length > 0 + tmpfile = File.new(redirStdout,"w") + save_stdout = $stdout.clone + $stdout.reopen(tmpfile) + end + + if redirStderr.length > 0 + tmpfile = File.new(redirStderr,"w") + save_stderr = $stderr.clone + $stderr.reopen(tmpfile) + end + + params = [ + 'L', # IN LPCSTR lpApplicationName + 'P', # IN LPSTR lpCommandLine + 'L', # IN LPSECURITY_ATTRIBUTES lpProcessAttributes + 'L', # IN LPSECURITY_ATTRIBUTES lpThreadAttributes + 'L', # IN BOOL bInheritHandles + 'L', # IN DWORD dwCreationFlags + 'L', # IN LPVOID lpEnvironment + 'L', # IN LPCSTR lpCurrentDirectory + 'P', # IN LPSTARTUPINFOA lpStartupInfo + 'P' # OUT LPPROCESS_INFORMATION lpProcessInformation + ] + returnValue = 'I' # BOOL + + startupInfo = [STARTUP_INFO_SIZE].pack('I') + ([0].pack('I') * (STARTUP_INFO_SIZE - 4)) + processInfo = [0].pack('I') * PROCESS_INFO_SIZE + + createProcess = Win32API.new("kernel32", "CreateProcess", params, returnValue) + + createProcess.call(0, command, 0, 0, 0, NORMAL_PRIORITY_CLASS, 0, 0, startupInfo, processInfo) + + if redirStdout.length > 0 + $stdout.reopen(save_stdout) + end + + save_stdout.close if save_stdout + + if redirStderr.length > 0 + $stderr.reopen(save_stderr) + end + + save_stderr.close if save_stderr + + ($0 == __FILE__ ) ? processInfo : processInfo.unpack("LLLL")[2] + end def Utils.is_absolute_path(path) - if HOST_OS.eql? "linux" or HOST_OS.eql? "darwin" then + if is_unix_like_os( HOST_OS ) then # if path start "/" then absoulte path if path.start_with?("/") then return true else return false end - elsif HOST_OS.eql? "windows" then + elsif is_windows_like_os( HOST_OS ) then # if path start "c:/" or "D:/" or ... then absoulte path if path =~ /^[a-zA-Z]:[\/]/ then return true @@ -204,9 +318,9 @@ class Utils # this will be used on MinGW/MSYS def Utils.get_unix_path(path) - if HOST_OS.eql? "linux" or HOST_OS.eql? "darwin" then + if is_unix_like_os( HOST_OS ) then return path - elsif HOST_OS.eql? "windows" then + elsif is_windows_like_os( HOST_OS ) then new_path = path if is_absolute_path( new_path ) then new_path = "/" + new_path[0,1] + new_path[2..-1] @@ -217,4 +331,234 @@ class Utils return path end end + + def Utils.file_lock(lock_file_name) + lock_file = File.new(lock_file_name, File::RDWR|File::CREAT, 0644) + lock_file.flock(File::LOCK_EX) + lock_file.rewind + lock_file.flush + lock_file.truncate(lock_file.pos) + + return lock_file + end + + def Utils.file_unlock(lock_file) + lock_file.close + end + + def Utils.parse_server_addr(saddr) + addr = saddr.split(":") + return nil unless addr.length == 2 + return addr + end + + def Utils.parse_ftpserver_url(surl) + return nil unless surl.start_with? "ftp://" + + surl = surl[6..-1] + parse1 = surl.split("@") + return nil unless parse1.length == 2 + + idpw = parse1[0] + url = parse1[1] + parse1 = idpw.split(":") + return nil unless parse1.length == 2 + + id = parse1[0] + passwd = parse1[1] + if url.end_with? "/" then url = url.chop end + + parse1 = url.split(":") + if parse1.length == 2 then + ip = parse1[0] + port = parse1[1] + elsif parse1.length == 1 then + ip = parse1[0] + port = 21 + else + return nil + end + + return [ip, port, id, passwd] + end + + + def Utils.generate_ftp_url(addr, port, username, passwd) + return "ftp://#{username}:#{passwd}@#{addr}:#{port}" + end + + + def Utils.extract_a_file(file_path, target_file, path) + dirname = File.dirname(file_path) + filename = File.basename(file_path) + ext = File.extname(filename) + + # path should be unix path if it is used in tar command + _package_file_path = Utils.get_unix_path(file_path) + _path = Utils.get_unix_path(path) + + case ext + when ".zip" then + if not path.nil? then + extract_file_command = "unzip -xo #{_package_file_path} #{target_file} -d #{_path}" + else + extract_file_command = "unzip -xo #{_package_file_path} #{target_file}" + end + when ".tar" then + if not path.nil? then + extract_file_command = "tar xf #{_package_file_path} -C #{_path} #{target_file}" + else + extract_file_command = "tar xf #{_package_file_path} #{target_file}" + end + end + + # check exit code + ret = execute_shell "#{extract_file_command}" + if not ret then return false end + + # check result file + if not path.nil? then + target_file_path = File.join(path, target_file) + else + target_file_path = target_file + end + + if not File.exist? target_file_path then + return false + else + return true + end + end + + + # check if the os is windows-like + def Utils.is_windows_like_os(os_name) + if os_name.start_with? "windows-" then + return true + else + return false + end + end + + + # check if the os is unix-like + def Utils.is_unix_like_os(os_name) + if os_name.start_with? "ubuntu-" or + os_name.start_with? "opensuse-" or + os_name.start_with?"macos-" then + return true + else + return false + end + end + + + # check if the os is linux-like + def Utils.is_linux_like_os(os_name) + if os_name.start_with? "ubuntu-" or + os_name.start_with? "opensuse-" then + return true + else + return false + end + end + + + # check if the os is macos-like + def Utils.is_macos_like_os(os_name) + if os_name.start_with?"macos-" then + return true + else + return false + end + end + + + def Utils.get_os_category(os_name) + if os_name.start_with? "ubuntu-" or os_name.start_with? "opensuse-" then + return "linux" + elsif os_name.start_with?"macos-" then + return "macos" + elsif os_name.start_with? "windows-" then + return "windows" + else + return os_name + end + end + + + def Utils.get_package_name_from_package_file( local_path ) + filename = File.basename(local_path) + if filename =~ /.*_.*_.*\.zip/ then + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + return new_name.split(",")[0] + end + return nil + end + + + def Utils.get_version_from_package_file( local_path ) + filename = File.basename(local_path) + if filename =~ /.*_.*_.*\.zip/ then + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + return new_name.split(",")[1] + end + return nil + end + + + def Utils.get_os_from_package_file( local_path ) + filename = File.basename(local_path) + if filename =~ /.*_.*_.*\.zip/ then + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + return new_name.split(",")[2] + end + return nil + end + + def Utils.multi_argument_test( arg, seperator ) + return ( not arg.end_with? seperator and not arg.split( seperator ).select{|x| x.empty?}.length > 0 ) + end + + def Utils.directory_emtpy?(dir_path) + return (Dir.entries(dir_path).join == "...") + end + + + def Utils.checksum(file_path) + if File.exist? file_path then + return `sha256sum #{file_path}`.split(" ")[0] + else + return nil + end + end + + def Utils.get_version() + version_file = "#{File.dirname(__FILE__)}/../../VERSION" + + if not File.exist? version_file then + return nil + end + + f = File.open( version_file, "r" ) + version = f.readline + f.close + + return version + end + + if defined?(HOST_OS).nil? then + HOST_OS = Utils.identify_current_OS() + end + + # set static variable in WORKING_DIR, HOME + if defined?(WORKING_DIR).nil? then WORKING_DIR = Dir.pwd end + if defined?(HOME).nil? then + # get home directory, using Dir.chdir + Dir.chdir + HOME = Dir.pwd + Dir.chdir WORKING_DIR + end + + end diff --git a/src/pkg_server/DistSync.rb b/src/pkg_server/DistSync.rb new file mode 100644 index 0000000..5fb6df9 --- /dev/null +++ b/src/pkg_server/DistSync.rb @@ -0,0 +1,98 @@ +=begin + + DistSync.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "packageServer.rb" +require "Action.rb" +require "ScheduledActionHandler.rb" + +class DistSyncAction < Action + + def initialize(time, pkgserver, dist_name ) + super(time, pkgserver.sync_interval) + + @pkgserver = pkgserver + @dist_name = dist_name + end + + + def init + end + + + def execute + # Start to sync job + @pkgserver.log.info "Executing sync action for the #{@dist_name}" + begin + execute_internal() + rescue => e + @pkgserver.log.error e.message + @pkgserver.log.error e.backtrace.inspect + end + end + + + private + def execute_internal() + # update pkg info + @pkgserver.reload_dist_package + + # sync + @pkgserver.sync( @dist_name, false ) + end +end + + +class DistSync + attr_accessor :quit + + # init + def initialize( server ) + @server = server + @handler = ScheduledActionHandler.new + end + + # start thread + def start() + # scan all sync distribution + @server.distribution_list.each do |dist| + # if dist does not have parent server then skip sync + if dist.server_url.empty? then next end + + time = Time.now + @server.log.info "Registered sync-action for dist : #{dist.name}" + @handler.register(DistSyncAction.new(time, @server, dist.name)) + end + + # start handler + @handler.start + end +end diff --git a/src/pkg_server/SocketRegisterListener.rb b/src/pkg_server/SocketRegisterListener.rb new file mode 100644 index 0000000..2ad78e3 --- /dev/null +++ b/src/pkg_server/SocketRegisterListener.rb @@ -0,0 +1,213 @@ +require 'socket' +require 'thread' +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/build_server" +require "packageServerConfig" +require "fileTransfer" +require "BuildComm" +require "net/ftp" + +# mutax for register operation +$register_mutex = Mutex.new + +class SocketRegisterListener + + # initialize + def initialize (parent) + @parent_server = parent + @thread = nil + @finish_loop = false + @log = @parent_server.log + end + + # start listening + def start() + @log.info "SocketRegisterListener start" + @thread = Thread.new { + main() + } + end + + # quit listening + def stop_listening() + @finish_loop = true + end + + private + + # thread main + def main() + @log.info "SocketRegisterListener entering main loop" + # server open + begin + @comm_server = BuildCommServer.create(@parent_server.port, @log) + rescue => e + @log.info "Server creation failed" + @log.error e.message + @log.error e.backtrace.inspect + return + end + + # loop + @log.info "Entering Control Listening Loop ... " + @finish_loop = false + @comm_server.wait_for_connection(@finish_loop) do |req| + begin + handle_job_request( req ) + rescue => e + @log.info "error occured in handle_job_request function" + @log.error e.message + @log.error e.backtrace.inspect + end + end + + # quit + @comm_server.terminate + end + + # wait for job requests + def wait_for_job_requests + req_list = [] + req_list.push @tcp_server.accept + + return req_list + end + + # handle job request + def handle_job_request( req ) + + # read request + req_line = req.gets + if req_line.nil? then return end + + # parse request + cmd = "" + if req_line.split("|").count > 0 then + cmd = req_line.split("|")[0].strip + end + + case cmd + when "UPLOAD" + Thread.new { + handle_cmd_upload( req_line, req ) + } + when "REGISTER" + Thread.new { + handle_cmd_register( req_line, req ) + } + when "STOP" + handle_cmd_stop( req_line, req ) + else + @log.error "Received Unknown REQ: #{req_line}" + end + @log.info "REQ processing done" + end + + # "UPLOAD" + def handle_cmd_upload( line, req ) + @log.info "Received File transfer REQ : #{line}" + + BuildCommServer.send_begin(req) + + tok = line.split("|").map { |x| x.strip } + if tok.count > 1 then + dock_name = tok[1].strip + incoming_dir = "#{@parent_server.incoming_path}/#{dock_name}" + FileUtils.mkdir_p(incoming_dir) + else + incoming_dir = "#{@parent_server.incoming_path}" + end + + file_path_list = [] + begin + @comm_server.receive_file(req, incoming_dir) + rescue => e + @log.error "Failed to transfer file" + @log.error e.message + @log.error e.backtrace.inspect + end + BuildCommServer.send_end(req) + end + + # "Register" + def handle_cmd_register( line, req ) + @log.info "Received register REQ : #{line}" + BuildCommServer.send_begin(req) + + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then + @log.error "Received Wrong REQ : #{line}" + BuildCommServer.send(req, "ERROR|Invalid REQ format") + return + end + dist_name = tok[1].strip + + if tok[2].start_with? "DOCK" then + dock_name = tok[3] + idx = 4 + else + dock_name = "" + idx = 2 + end + + file_path_list = [] + + while idx < tok.length do + if dock_name.empty? then + file_path_list.push "#{@parent_server.incoming_path}/#{tok[idx]}" + else + file_path_list.push "#{@parent_server.incoming_path}/#{dock_name}/#{tok[idx]}" + end + idx = idx + 1 + end + # register mutex + $register_mutex.synchronize { + begin + @parent_server.reload_dist_package() + snapshot_name = @parent_server.register( file_path_list, dist_name, true, false, true) + BuildCommServer.send(req,"SUCC|#{snapshot_name}") + rescue => e + @log.error "register failed" + @log.error e.message + @log.error e.backtrace.inspect + BuildCommServer.send(req, "ERROR|#{e.message}") + @parent_server.release_lock_file + return + end + } + + if not dock_name.empty? then + FileUtils.rm_rf "#{@parent_server.incoming_path}/#{dock_name}" + end + + BuildCommServer.send_end(req) + end + + # "STOP" + def handle_cmd_stop( line, req ) + @log.info "Received STOP REQ" + + BuildCommServer.send_begin(req) + + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + @log.error "Received Wrong REQ : #{line}" + BuildCommServer.send(req, "ERROR|Invalid REQ format") + return + end + passwd = tok[1].strip + + if @parent_server.passwd.eql? passwd then + @parent_server.finish = true + @log.info "Package server stop flag set" + BuildCommServer.send(req,"SUCC") + else + @log.info "Received stop command, but passwd mismatched : #{passwd}" + BuildCommServer.send(req,"ERROR|Password mismatched!") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end +end + diff --git a/src/pkg_server/client.rb b/src/pkg_server/client.rb index 5cf8423..a7e2034 100644 --- a/src/pkg_server/client.rb +++ b/src/pkg_server/client.rb @@ -1,6 +1,5 @@ - =begin - + client.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -28,29 +27,36 @@ Contributors: =end require "fileutils" +require "thread" $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/build_server" require "downloader" require "installer" +require "fileTransfer" require "packageServerConfig" require "package" require "parser" require "utils" require "log" require "Version" - +require "net/ftp" +$update_mutex = Mutex.new +$get_snapshot_mutex = Mutex.new +$filemove_mutex = Mutex.new class Client # constant - SUPPORTED_OS = ["linux", "windows", "darwin"] PKG_LIST_FILE_PREFIX = "pkg_list_" INSTALLED_PKG_LIST_FILE = "installedpackage.list" CONFIG_PATH = "#{PackageServerConfig::CONFIG_ROOT}/client" PACKAGE_INFO_DIR = ".info" DEFAULT_INSTALL_DIR = "#{Utils::HOME}/build_root" DEFAULT_SERVER_ADDR = "http://172.21.17.55/dibs/unstable" + OS_INFO_FILE = "os_info" + ARCHIVE_PKG_LIST_FILE = "archive_pkg_list" - attr_accessor :server_addr, :location, :pkg_hash_os, :is_server_remote, :installed_pkg_hash_loc, :archive_pkg_list, :all_dep_list, :log + attr_accessor :server_addr, :location, :pkg_hash_os, :is_server_remote, :installed_pkg_hash_loc, :archive_pkg_list, :all_dep_list, :log, :support_os_list, :config_dist_path, :download_path, :tmp_path, :snapshot_path, :snapshots_path, :snapshot_url public # initialize @@ -64,348 +70,354 @@ class Client # set default server address, location if server_addr.nil? then server_addr = get_default_server_addr() end - if location.nil? then location = get_default_inst_dir() end + if location.nil? then location = get_default_inst_dir() end # chop server address, if end with "/" - if server_addr.strip.end_with? "/" then server_addr = server_addr.chop end + if server_addr.strip.end_with? "/" then server_addr = server_addr.chop end + + @snapshot_path = nil + @snapshot_url = false + + if is_snapshot_url(server_addr) then + @snapshot_url = true + @server_addr, @snapshot_path = split_addr_and_snapshot(server_addr) + else + @server_addr = server_addr + end - @server_addr = server_addr @location = location @pkg_hash_os = {} @installed_pkg_hash_loc = {} @archive_pkg_list = [] @all_dep_list = [] @is_server_remote = Utils.is_url_remote(server_addr) + @support_os_list = [] + @config_dist_path = CONFIG_PATH + "/" + get_flat_serveraddr + @download_path = @config_dist_path + "/downloads" + @tmp_path = @config_dist_path + "/tmp" + @snapshots_path = @config_dist_path + "/snapshots" + + # create directory + if not File.exist? @config_dist_path then FileUtils.mkdir_p "#{@config_dist_path}" end + if not File.exist? @download_path then FileUtils.mkdir_p "#{@download_path}" end + if not File.exist? @snapshots_path then FileUtils.mkdir_p "#{@snapshots_path}" end + if not File.exist? @tmp_path then FileUtils.mkdir_p "#{@tmp_path}" end # set log if logger.nil? or logger.class.to_s.eql? "String" then @log = Log.new(logger) else @log = logger - end - - FileInstaller.set_logger(@log) - FileDownLoader.set_logger(@log) + end # read installed pkg list, and create hash - FileUtils.mkdir_p "#{@location}" - create_installed_pkg_hash() - - # readk remote pkg list, and hash list - create_remote_pkg_hash(false) - @log.info "Initialize - #{server_addr}, #{location}" - end + if not File.exist? @location then FileUtils.mkdir_p "#{@location}" end + @log.info "Update local package list.. [#{@location}]" + read_installed_pkg_list() + + # read remote pkg list, and hash list + @log.info "Update remote package list and supported os list.." + update() + @log.info "Initialize - #{server_addr}, #{location}" + end public # update package list from server def update() - if not create_remote_pkg_hash(true) then - @log.error "\"#{@server_addr}\" does not have package list file properly." - return false - end - create_default_config(@server_addr) - @log.info "Update package list from \"#{@server_addr}\".. OK" + if not @snapshot_url then + $get_snapshot_mutex.synchronize { + @snapshot_path = get_lastest_snapshot(@is_server_remote) + } + end + @log.info "The lastest snapshot : #{@snapshot_path}" + if @snapshot_path.nil? then + @log.warn "Failed to get the lastest package list" + @snapshot_path = "" + end + + exists_snapshot = false + if is_snapshot_exist(@snapshot_path) then + @log.info "Snapshot information is already cached [#{get_pkglist_path()}]" + exists_snapshot = true + else + @log.info "Snapshot information is not cached" + end + + list_path = get_pkglist_path() + if list_path.nil? then + @log.error "Failed to get package list path" + return false + end + + clean_list() + + if exists_snapshot then + read_supported_os_list(list_path) + read_remote_pkg_list(list_path) + read_archive_pkg_list(list_path) + else + $update_mutex.synchronize { + uniq_name = Utils.create_uniq_name + tmp_dir = File.join(@config_dist_path, uniq_name) + FileUtils.mkdir_p tmp_dir + if not download_os_list(@is_server_remote, tmp_dir) then + @log.error "\"#{@server_addr}\" does not have supported os list file properly." + Utils.execute_shell("rm -rf #{tmp_dir}") + return false + else read_supported_os_list(tmp_dir) end + + if not download_pkg_list(@is_server_remote, tmp_dir) then + @log.error "\"#{@server_addr}\" does not have package list file properly." + Utils.execute_shell("rm -rf #{tmp_dir}") + return false + else read_remote_pkg_list(tmp_dir) end + + if not download_archive_pkg_list(@is_server_remote, tmp_dir) then + @log.error "\"#{@server_addr}\" does not have archive package list file properly. This error can be ignored" + else read_archive_pkg_list(tmp_dir) end + Utils.execute_shell("mv #{tmp_dir} #{list_path}") + @log.info "Moved \"#{tmp_dir}\" to" + @log.info " \"#{list_path}\"" + # tmp_dir should be removed whether mv command is failed + Utils.execute_shell("rm -rf #{tmp_dir}") + remove_snapshots() + } + end + + $update_mutex.synchronize { + create_default_config(@server_addr) + @log.info "Update package list from \"#{@server_addr}\".. OK" + } + return true - end + end + + private + def clean_list() + @pkg_hash_os.clear + @archive_pkg_list.clear + @support_os_list.clear + @log.info "Cleard package list, supported os list.. OK" + end public # download package - def download(pkg_name, os, trace) + def download(pkg_name, os, trace, loc = nil) + + if loc.nil? then loc = @location end dependent_pkg_list = [] # get dependent list if trace then - dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, false) + dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, true) if dependent_pkg_list.nil? then @log.error "Failed to get dependency for \"#{pkg_name}\" package" return nil end else dependent_pkg_list = [pkg_name] end - surl = nil - addr_arr = @server_addr.split('/') - if addr_arr[-2].eql? "snapshots" then - surl = @server_addr + "/../.." - else - surl = @server_addr - end - + surl = @server_addr # download files file_local_path = [] dependent_pkg_list.each do |p| + pkg_name = get_attr_from_pkg(p, os, "name") pkg_path = get_attr_from_pkg(p, os, "path") pkg_ver = get_attr_from_pkg(p, os, "version") + pkg_checksum = get_attr_from_pkg(p, os, "checksum") + pkg_size = get_attr_from_pkg(p, os, "size") if pkg_path.nil? or pkg_ver.nil? then @log.error "\"#{p}\" package does not exist in package server. If it exist in package server, then try \"pkg-cli update\"" return nil end url = surl + pkg_path filename = pkg_path.split('/')[-1] - if not FileDownLoader.download(url, @location) then - @log.error "Failed download #{pkg_name} [#{pkg_ver}]" - return nil + + if not FileDownLoader.download(url, loc, @log) then end - file_path = File.join(@location, filename) + file_path = File.join(loc, filename) file_local_path.push(file_path) - @log.info "Downloaded \"#{p} [#{pkg_ver}]\" package file.. OK" - #@log.info " [path : #{file_path}]" end if trace then @log.info "Downloaded \"#{pkg_name}\" package with all dependent packages.. OK" - else - @log.info "Downloaded only \"#{pkg_name}\" package.. OK" end - @log.info " [path : #{file_local_path.join(", ")}]" + @log.info " [path: #{file_local_path.join(", ")}]" return file_local_path - end - - public - # download source package - def download_source(pkg_name, os) - - # get source file path - src_path = get_attr_from_pkg(pkg_name, os, "src_path") - if src_path.nil? or src_path.empty? then - @log.error "#{pkg_name} package does not have source" - return nil - end - file_url = nil - - addr_arr = @server_addr.split('/') - if addr_arr[-2].eql? "snapshots" then - surl = @server_addr + "/../.." + src_path - else - surl = @server_addr + src_path - end + end - # download file - filename = src_path.split('/')[-1] - if not FileDownLoader.download(surl, @location) then - @log.error "Failed download #{pkg_name} source" - return nil - end - file_local_path = File.join(@location, filename) - @log.info "Downloaded source of #{pkg_name} package.. OK" - @log.info " [path : #{file_local_path}]" + private + def remove_downloaded_pkgs(pkg_name, os) + pkg_file_prefix = "#{@download_path}/#{pkg_name}_*_#{os}.zip" + pkg_files = Dir[pkg_file_prefix].sort_by { |f| File.mtime(f) }.reverse + + if not pkg_files.nil? and pkg_files.length >= 4 then + Utils.execute_shell("rm -rf #{pkg_files[3..-1].join(" ")}") + @log.info "Removed old package files.." + @log.info " * #{pkg_files[3..-1].join(", ")}" + end + end + + private + def move_downloaded_pkg(filepath, distpath) + if filepath.nil? or filepath == "" then return nil end + filename = filepath.split('/')[-1] + if not File.exist? distpath then FileUtils.mkdir_p "#{distpath}" end + distfile = File.join(distpath, filename) + @log.info "Moving \"#{filename}\" to download cache directory" + @log.info " [path: #{distpath}]" + $filemove_mutex.synchronize { + if not File.exist? distfile then + Utils.execute_shell("mv #{filepath} #{distfile}") + else + Utils.execute_shell("rm -f #{filepath}") + return distfile + end + } + + if File.exist? distfile then return distfile + else + @log.info "Failed to move [#{filenamae}] to " + @log.info " [#{distpath}]" + return nil + end + end + + private + def remove_snapshots() + listing_prefix = "#{@snapshots_path}/*" + dirs = Dir[listing_prefix].sort_by { |f| File.mtime(f) }.reverse + + if not dirs.nil? and dirs.length >= 20 then + Utils.execute_shell("rm -rf #{dirs[19..-1].join(" ")}") + @log.info "Removed old snapshots.." + @log.info " * #{dirs[19]} ~ " + end + end + + private + def get_cached_filepath(pkg_filename, pkg_checksum, pkg_size) + + cached_filepath = "#{@download_path}/#{pkg_filename}" + if File.exist? cached_filepath then + checksum = `sha256sum #{cached_filepath}`.split(" ")[0] + size = `du -b #{cached_filepath}`.split[0].strip + if checksum.eql? pkg_checksum and size.eql? pkg_size then + return cached_filepath + end + end + return nil + end - return file_local_path - end - public # download dependent source def download_dep_source(file_name) - file_url = nil - - addr_arr = @server_addr.split('/') - if addr_arr[-2].eql? "snapshots" then - file_url = @server_addr + "/../../source/" + file_name - else - file_url = @server_addr + "/source/#{file_name}" - end - if not FileDownLoader.download(file_url, @location) then + file_url = @server_addr + "/source/#{file_name}" + if not FileDownLoader.download(file_url, @location, @log) then @log.error "Failed download #{file_name}" return nil end file_local_path = File.join(@location, file_name) @log.info "Downloaded \"#{file_name}\" source file.. OK" - @log.info " [path : #{file_local_path}]" + @log.info " [path: #{file_local_path}]" return file_local_path end - public - # check archive file - def check_archive_file(file_name) - - result = false - filename = "archive_pkg_list" - local_file_path = File.join(CONFIG_PATH, filename) - if File.exist? local_file_path then - File.open(local_file_path, "r") do |f| - f.each_line do |l| - if l.strip.eql? file_name.strip then - result = true - break - end - end - end - end - return result - end - public # upload package - def upload(ssh_alias, id, binary_path_list, source_path_list, verify) + def upload(ip, port, ftp_addr, ftp_port, ftp_username, ftp_passwd, binary_path_list) - # check source path list - if source_path_list.nil? or source_path_list.empty? then - @log.error "source package path should be set." - return nil - end - - # verify ssh alias - verify = false - hostfound = false - sshconfig = "#{Utils::HOME}/.ssh/config" - File.open(sshconfig, "r") do |f| - f.each_line do |l| - if l.strip.upcase.start_with? "HOST" then - al = l.strip.split(' ')[1].strip - if al.eql? ssh_alias then hostfound = true - else next end - end - end - end + # check ip and port + if ip.nil? or port.nil? then + @log.error "Ip and port should be set." + return nil + end - if not hostfound then - @log.error "\"#{ssh_alias}\" does not exist in \".ssh/config\" file" + # check binary path list + if binary_path_list.nil? or binary_path_list.empty? then + @log.error "Binary package path should be set." return nil end - # get distribution from server addr - dist = get_distribution() - if dist.nil? then - @log.error "Distribution is nil" - return nil - end - - serveraddr = @server_addr - snapshot = nil - if serveraddr.include? "snapshots" then snapshot = serveraddr.split("/")[-1] end - - # set server homd directory - server_home = `ssh #{ssh_alias} pwd` - server_home = server_home.strip - - # set "pkg-svr" file path - # if pkg-svr exist in path then using pkg-svr - result = `ssh #{ssh_alias} which pkg-svr` - if not( result.nil? or result.empty? or result.strip.empty? ) then - pkg_svr = "pkg-svr" - else - # if pkg-svr not exist in path then try ~/tizen_sdk/dev_tools/pkg-svr - result = `ssh #{ssh_alias} which #{server_home}/tizen_sdk/dev_tools/pkg-svr` - if not( result.nil? or result.empty? or result.strip.empty? ) then - pkg_svr = "#{server_home}/tizen_sdk/dev_tools/pkg-svr" - else - @log.error "Can't find server's pkg-svr command" - return nil - end - end - pkg_svr = "#{server_home}/tizen_sdk/dev_tools/pkg-svr" + # create unique dock number + dock = Utils.create_uniq_name() - # set incoming directory (~/.build_tools/pkg_server/#{id}/incoming) - incoming_path = "#{server_home}/.build_tools/pkg_server/#{id}/incoming" + # upload file + binary_list = [] + binary_path_list.each do |bpath| + filename = File.basename(bpath) + client = BuildCommClient.create(ip, port, @log) - # set pkg-svr register command - register_command = "#{pkg_svr} register -i #{id} -d #{dist}" + if client.nil? then + @log.error "Failed to create BuildCommClient instance.." + return nil + end + + @log.info "Send ready REQ.. [UPLOAD]" + result = client.send("UPLOAD|#{dock}") + if not result then + @log.error "Failed to send ready REQ.." + return nil + end - # upload source package (scp) - server_src_pkg_list_command = "\"" - source_path_list.each do |spath| - # set source package file path for server filesystem - src_file_name = File.basename(spath) - server_src_pkg_path = "#{incoming_path}/#{src_file_name}" - server_src_pkg_list_command = server_src_pkg_list_command + server_src_pkg_path + "," - # upload source package - if File.exist? spath then - Utils.execute_shell("cd #{File.dirname(spath)};scp #{File.basename(spath)} #{ssh_alias}:#{server_src_pkg_path}") - else - @log.error "#{spath} file does not exist" + begin + result = client.send_file(ftp_addr, ftp_port, ftp_username, ftp_passwd, bpath) + rescue => e + @log.error "FTP failed to put file (exception)" + @log.error "#{e.message}" + @log.error e.backtrace.inspect return nil end - end - server_src_pkg_list_command = server_src_pkg_list_command.strip - if server_src_pkg_list_command.end_with? "," then - server_src_pkg_list_command = server_src_pkg_list_command.chop + "\"" - else - server_src_pkg_list_command = server_src_pkg_list_command + "\"" - end + if not result then + @log.error "FTP failed to put file (result is false)" + return nil + end - # add src package list to register command - register_command = register_command + " -s #{server_src_pkg_list_command} -g" + client.terminate + binary_list.push(filename) + end - # upload binary package (scp) - if not binary_path_list.nil? then - server_bin_pkg_list_command = "\"" - binary_path_list.each do |bpath| - bin_file_name = File.basename(bpath) - bin_pkg_name = bin_file_name.split("_")[0] - if verify then - if not verify_upload(bin_pkg_name, bpath) then - @log.error "Failed to verify \"#{bpath}\" file" + # register file + if not binary_list.empty? then + client = BuildCommClient.create(ip, port, @log) + dist = get_distribution + if dist.empty? then + @log.error "Distribution is empty.." + return nil + end + + @log.info "Send register message.. [REGISTER|#{dist}|DOCK|#{dock}|#{binary_list.join("|")}]" + snapshot = nil + if client.send "REGISTER|#{dist}|DOCK|#{dock}|#{binary_list.join("|")}" then + output = client.read_lines do |l| + line = l.split("|") + if line[0].strip == "ERROR" then + @log.error l.strip return nil + elsif line[0].strip == "SUCC" then + snapshot = line[1].strip end end - - server_bin_pkg_path = "#{incoming_path}/#{bin_file_name}" - server_bin_pkg_list_command = server_bin_pkg_list_command + server_bin_pkg_path + "," - # upload binary package - if File.exist? bpath then - Utils.execute_shell("cd #{File.dirname(bpath)};scp #{File.basename(bpath)} #{ssh_alias}:#{server_bin_pkg_path}") - else - @log.error "#{bpath} file does not exist" + if not output then + @log.error "Failed to register" return nil end - end + end - server_bin_pkg_list_command = server_bin_pkg_list_command.strip - if server_bin_pkg_list_command.end_with? "," then - server_bin_pkg_list_command = server_bin_pkg_list_command.chop + "\"" - else - server_bin_pkg_list_command = server_bin_pkg_list_command + "\"" + client.terminate + snapshot = @server_addr + "/snapshots/" + snapshot + @log.info "Registered successfully! [#{binary_path_list.join("|")}]" + if snapshot.empty? then + @log.error "Failed to generate snapshot" end + end - # add bin package list to register command - register_command = register_command + " -p #{server_bin_pkg_list_command}" - end - - @log.info "register_command : #{register_command}" - - # register packages to server - result = `ssh #{ssh_alias} #{register_command}` - if result.strip.include? "Error occured" then - puts result - return nil - end - - # parsing snapshot url to show user - serveraddr = @server_addr - arr = serveraddr.split("/") - if serveraddr.include? "snapshots" then sid = arr[-4] - else sid = arr[-2] end - i = serveraddr.index(sid) - serveraddr = serveraddr[0..i-1] - serveraddr = serveraddr + id + "/" + dist - - addr = [] - result2 = "" - arr_re = result.split("\n") - arr_re.each do |l| - l = l.strip - if l.start_with? "snapshot is generated :" then - addr = l.split(":")[1].split("/") - if addr.include? dist then - i = addr.index(dist) - addr = addr[i+1..-1] - str = "" - addr.each do |l| str = str + "/" + l end - str = serveraddr.strip + str - result2 = result2 + str +"\n" - end - end - end - - @log.info "Upload packages.. OK" - @log.info " [#{binary_path_list.join(", ")}]" - @log.info " [#{source_path_list.join(", ")}]" - return result2 + return snapshot end private @@ -415,19 +427,30 @@ class Client manifest_file = "pkginfo.manifest" uniq_name = Utils.create_uniq_name path = Utils::HOME + "/tmp/#{uniq_name}" - FileUtils.mkdir_p "#{path}" - if not FileInstaller.extract_specified_file(pkg_path, manifest_file, path) then - @log.error "The \"pkginfo.manifest\" file does not exist in \"#{pkg_path}\"" - return false - end - manifest_path = File.join(path, manifest_file) - pkg_hash = Parser.read_pkg_list(manifest_path) - FileUtils.rm_f(manifest_path) - FileUtils.remove_dir(path, true) - - new_pkg_ver = pkg_hash[pkg_name].version - new_pkg_install_dep_list = pkg_hash[pkg_name].install_dep_list - os = pkg_hash[pkg_name].os + if not File.exist? path then FileUtils.mkdir_p "#{path}" end + begin + if not FileInstaller.extract_a_file(pkg_path, manifest_file, path, @log) then + @log.error "The \"pkginfo.manifest\" file does not exist in \"#{pkg_path}\"" + return false + end + manifest_path = File.join(path, manifest_file) + pkg = Parser.read_single_pkginfo_from manifest_path + if File.exists? manifest_path then FileUtils.rm_f(manifest_path) end + FileUtils.remove_dir(path, true) + rescue Interrupt + @log.error "Client: Interrupted.." + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + raise Interrupt + rescue RuntimeError => e + @log.error( e.message, Log::LV_USER) + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + return false + end + new_pkg_ver = pkg.version + new_pkg_install_dep_list = pkg.install_dep_list + os = pkg.os list = get_all_reverse_install_dependent_packages_remote(pkg_name, os, true) @@ -474,20 +497,36 @@ class Client end dist = "" - server_arr = server.split("/") - if server_arr.include? "snapshots" then - i = server_arr.index("snapshots") - dist = server_arr[i-1] - else dist = File.basename(server) end + dist = File.basename(server) return dist - end + end + + private + def get_flat_serveraddr() + server = @server_addr + if server.nil? or server.empty? then + @log.error "Server addr is nil" + return nil + end + + server = server.delete ".:/@" + return server + end public # install package # install all install dependency packages def install(pkg_name, os, trace, force) + ret = install_internal( pkg_name, os, trace, force ) + return ret + end + + + private + def install_internal(pkg_name, os, trace, force) + if trace.nil? then trace = true end if force.nil? then force = false end @@ -501,43 +540,44 @@ class Client @log.error "#{pkg_name} package does not exist in remote package list" return false end + compare_result = compare_version_with_installed_pkg(pkg_name, pkg_ver) if not force then - case compare_result - when -1 then - @log.warn "\"#{pkg_name}\" package version is bigger then remote package version" - return true - when 0 then - @log.warn "\"#{pkg_name}\" package version is same with remote package version" - return true - when 1, 2 then - end - end - - # if enable trace, create all dependent package list - if trace then - dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, force) - if dependent_pkg_list.nil? then - @log.error "Failed to get dependency for \"#{pkg_name}\" package" - return false - end - else - dependent_pkg_list = [pkg_name] - end - - # TODO: need to compare dependent package version - # install packages including dependent packages - dependent_pkg_list.each do |pkg| - if not install_pkg(pkg, os, force) then - @log.error "#{pkg} does not exist" - return false - end - add_pkg_info(pkg, os) - end - - # write installed package information to file - write_pkg_hash_to_file(nil) + case compare_result + when -1 then + @log.warn "Checked \"#{pkg_name}\" package version : it is bigger then remote package version" + return true + when 0 then + @log.warn "Checked \"#{pkg_name}\" package version : it is same with remote package version" + return true + when 1, 2 then + end + end + + # if enable trace, create all dependent package list + if trace then + dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, force) + if dependent_pkg_list.nil? then + @log.error "Failed to get dependency for \"#{pkg_name}\" package" + return false + end + else + dependent_pkg_list = [pkg_name] + end + + # TODO: need to compare dependent package version + # install packages including dependent packages + dependent_pkg_list.each do |pkg| + if not install_pkg(pkg, os, force) then + @log.error "#{pkg} does not exist" + return false + end + add_pkg_info(pkg, os) + end + + # write installed package information to file + write_pkg_hash_to_file(nil) if trace then @log.info "Installed \"#{pkg_name} [#{pkg_ver}]\" package with all dependent packages.. OK" @@ -545,12 +585,22 @@ class Client else @log.info "Install only \"#{pkg_name} [#{pkg_ver}]\" package.. OK" end + return true end + public # install local package (ignore dependent packages) - def install_local_pkg(pkg_path, force) + def install_local_pkg(pkg_path, trace, force, repos_paths = nil) + + ret = install_local_pkg_internal(pkg_path, trace, force, repos_paths) + return ret + end + + + private + def install_local_pkg_internal(pkg_path, trace, force, repos_paths) file_name = File.basename(pkg_path) pkg_name = file_name.split('_')[0] @@ -566,50 +616,93 @@ class Client return false end pkg_name = filename.split("_")[0] - type = "binary" manifest_file = "pkginfo.manifest" - pkg_config_path = File.join(@location, PACKAGE_INFO_DIR, pkg_name) uniq_name = Utils.create_uniq_name path = Utils::HOME + "/tmp/#{uniq_name}" - FileUtils.mkdir_p "#{path}" - if not FileInstaller.extract_specified_file(pkg_path, manifest_file, path) then - @log.error "pkginfo.manifest file does not exist in #{pkg_path}" - return false - end - manifest_path = File.join(path, manifest_file) - pkg_hash = Parser.read_pkg_list(manifest_path) - new_pkg_ver = pkg_hash[pkg_name].version - FileUtils.rm_f(manifest_path) - FileUtils.remove_dir(path, true) - - compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) - if not force then - case compare_result - when -1 then - @log.warn "\"#{pkg_name}\" package version is bigger then remote package version.." - return true - when 0 then - @log.warn "\"#{pkg_name}\" package version is same with remote package version.." - return true - when 1, 2 then + if not File.exist? path then FileUtils.mkdir_p "#{path}" end + begin + if not FileInstaller.extract_a_file(pkg_path, manifest_file, path, @log) then + @log.error "pkginfo.manifest file does not exist in #{pkg_path}" + return false end - end + manifest_path = File.join(path, manifest_file) + pkg = Parser.read_single_pkginfo_from manifest_path + new_pkg_ver = pkg.version + FileUtils.remove_dir(path, true) + rescue Interrupt + @log.error "Client: Interrupted.." + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + raise Interrupt + rescue RuntimeError => e + @log.error( e.message, Log::LV_USER) + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + return false + end + + compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + if not force then + case compare_result + when -1 then + @log.warn "Installed \"#{pkg_name}\" package version is bigger.." + return true + when 0 then + @log.warn "Checked \"#{pkg_name}\" package version : it is same with installed package version" + return true + when 1, 2 then + end + end + + if check_installed_pkg(pkg_name) then + uninstall(pkg_name, false) + end + + if trace then + install_dep_pkgs = pkg.install_dep_list + new_pkg_os = pkg.os + install_dep_pkgs.each do |p| + # check local path first + if not repos_paths.nil? then + # search + binpkgs = [] + repos_paths.each { |repos_path| + binpkgs += Dir.glob("#{repos_path}/#{p.package_name}_*_#{new_pkg_os}.zip") + } + if not binpkgs.empty? then + if not install_local_pkg_internal(binpkgs[0], true, false, repos_paths) then + @log.warn "#{p} package is not installed" + end + else + if not install_internal(p.package_name, new_pkg_os, true, false) then + @log.warn "#{p} package is not installed" + end + end + else + if not install_internal(p.package_name, new_pkg_os, true, false) then + @log.warn "#{p} package is not installed" + end + end + end + end - if check_installed_pkg(pkg_name) then - uninstall(pkg_name, false) - end + # install package + ret = FileInstaller.install(pkg_name, pkg_path, "binary", @location, @log) - # install package - ret = FileInstaller.install(pkg_name, pkg_path, "binary", @location) + if not ret then + @log.error "Install failed \"#{pkg_path} [#{new_pkg_ver}]\" file.. " + return false + end - add_local_pkg_info(pkg_name) - write_pkg_hash_to_file(nil) + add_local_pkg_info(pkg_name) + write_pkg_hash_to_file(nil) @log.info "Installed \"#{pkg_path} [#{new_pkg_ver}]\" file.. OK" return true end + public # upgrade package def upgrade(os, trace) @@ -630,7 +723,7 @@ class Client end end - if not install(p, os, trace, false) then + if not install_internal(p, os, trace, false) then @log.error "Failed to install \"#{p}\" package.." return false end @@ -753,7 +846,7 @@ class Client pkg_list.each do |p| if not check_installed_pkg(p) then next end - if not FileInstaller.uninstall(p, type, @location) then + if not FileInstaller.uninstall(p, type, @location, @log) then @log.error "Failed uninstall \"#{pkg_name}\" package" return false end @@ -784,11 +877,11 @@ class Client return end end - FileUtils.rm_rf(@location) + if File.exist? @location then FileUtils.rm_rf(@location) end FileUtils.mkdir_p(@location) - @pkg_hash_os.clear + #@pkg_hash_os.clear @installed_pkg_hash_loc.clear - @archive_pkg_list.clear + #@archive_pkg_list.clear @log.info "Cleaned \"#{@location}\" path.. OK" end @@ -798,13 +891,14 @@ class Client result = [] pkg_hash = @pkg_hash_os[os] + if pkg_hash.nil? then return [] end pkg_list = pkg_hash.values pkg_list.each do |pkg| pkg.build_dep_list.each do |dep| if dep.package_name.eql? pkg_name and not dep.target_os_list.nil? and dep.target_os_list.include? os then - result.push(pkg.package_name) + result.push(pkg) end end end @@ -814,18 +908,20 @@ class Client public # get reverse source dependent packages (just 1 depth) - def get_reverse_source_dependent_packages(pkg_name, os) + def get_reverse_source_dependent_packages(pkg_name) result = [] - pkg_hash = @pkg_hash_os[os] - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.source_dep_list.each do |p| - if p.package_name.eql? pkg_name then - result.push(pkg.package_name) - end - end - end + @support_os_list.each do |os| + pkg_hash = @pkg_hash_os[os] + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.source_dep_list.each do |p| + if p.package_name.eql? pkg_name then + result.push(pkg) + end + end + end + end return result end @@ -938,7 +1034,7 @@ class Client i = i + 1 end - @log.info "Get install dependent packages for #{pkg_name} package.. OK" + @log.info "Get install dependent packages for \"#{pkg_name}\" package.. OK" if reverse then return result.reverse.uniq.push(pkg_name) else return result.uniq.insert(0, pkg_name) end end @@ -998,7 +1094,7 @@ class Client if pkg_hash.nil? then return false end pkg = pkg_hash[pkg_name] if pkg.nil? then - @log.warn "There is no \"#{pkg_name}\" remote package information in list" + #@log.warn "There is no \"#{pkg_name}\" remote package information in list" return false end @@ -1048,6 +1144,7 @@ class Client if pkg.nil? then return nil end case attr + when "name" then return pkg.package_name when "path" then return pkg.path when "source" then return pkg.source when "version" then return pkg.version @@ -1056,6 +1153,9 @@ class Client when "build_dep_list" then return pkg.build_dep_list when "install_dep_list" then return pkg.install_dep_list when "attribute" then return pkg.attribute + when "checksum" then return pkg.checksum + when "size" then return pkg.size + end end @@ -1209,11 +1309,12 @@ class Client return s end - private + public def get_pkg_from_list(pkg_name, os) pkg_hash = @pkg_hash_os[os] if pkg_hash.nil? then return nil end + pkg = pkg_hash[pkg_name] return pkg @@ -1246,16 +1347,20 @@ class Client # below code should be changed type = path.split('/')[-2] new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") + pkg_checksum = get_attr_from_pkg(pkg_name, os, "checksum") + pkg_size = get_attr_from_pkg(pkg_name, os, "size") + pkg_path = get_attr_from_pkg(pkg_name, os, "path") + filename = pkg_path.split('/')[-1] # compare version with installed package versiona compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) if not force then case compare_result when -1 then - @log.warn "\"#{pkg_name}\" package version is bigger then remote package version" + @log.warn "Checked \"#{pkg_name}\" package version : it is bigger then remote package version" return true when 0 then - @log.warn "\"#{pkg_name}\" package version is same with remote package version" + @log.warn "Checked \"#{pkg_name}\" package version : it is same with remote package version" return true end end @@ -1268,32 +1373,34 @@ class Client end end - # download package file - # change download location temporary (back to the origin path after downloading) - loc_back = @location - uniq_name = Utils.create_uniq_name - tmppath = Utils::HOME + "/tmp/#{uniq_name}" - FileUtils.mkdir_p "#{tmppath}" - @location = tmppath - file_local_path = download(pkg_name, os, false)[0] - @location = loc_back - if file_local_path.nil? then - FileUtils.remove_dir(tmppath, true) - return false - end - # install package - ret = FileInstaller.install(pkg_name, file_local_path, type, @location) - FileUtils.rm_f(file_local_path) - FileUtils.remove_dir(tmppath, true) + cached_filepath = nil + if Utils.is_linux_like_os( Utils::HOST_OS ) then + cached_filepath = get_cached_filepath(filename, pkg_checksum, pkg_size) + end + if not cached_filepath.nil? then + @log.info "Cached #{pkg_name} package file.. OK" + ret = FileInstaller.install(pkg_name, cached_filepath, type, @location, @log) + else + filepath = download(pkg_name, os, false, @tmp_path) + if filepath.nil? then + return false + end + filepath = move_downloaded_pkg(filepath[0], @download_path) + if filepath.nil? then + return false + end + ret = FileInstaller.install(pkg_name, filepath, type, @location, @log) + remove_downloaded_pkgs(pkg_name, os) + end return ret - end + end private - def compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + def compare_version_with_installed_pkg(pkg_name, new_pkg_ver) if check_installed_pkg_list_file() then - create_installed_pkg_hash() + read_installed_pkg_list() if check_installed_pkg(pkg_name) then installed_pkg_ver = get_attr_from_installed_pkg(pkg_name, "version") compare_result = Utils.compare_version(installed_pkg_ver, new_pkg_ver) @@ -1332,14 +1439,14 @@ class Client else pkg_hash[pkg_name] = get_pkg_from_list(pkg_name, os) end @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - @log.info "Added information for \"#{pkg_name}\" package.. OK" + #@log.info "Added information for \"#{pkg_name}\" package.. OK" return pkg_hash end private # add package manifest info def add_local_pkg_info(pkg_name) - + config_path = File.join(@location, PACKAGE_INFO_DIR, "#{pkg_name}") pkg = read_pkginfo_file(pkg_name, config_path) @@ -1356,7 +1463,7 @@ class Client else pkg_hash[pkg_name] = pkg end @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - @log.info "Added information for \"#{pkg_name}\" package.. OK" + #@log.info "Added information for \"#{pkg_name}\" package.. OK" return pkg_hash end @@ -1365,75 +1472,203 @@ class Client def read_pkginfo_file(pkg_name, path) file_path = File.join(path, "pkginfo.manifest") - pkg_hash = Parser.read_pkg_list(file_path) + begin + pkg = Parser.read_single_pkginfo_from file_path + rescue => e + @log.error( e.message, Log::LV_USER) + return nil + end - if pkg_hash.nil? then + if pkg.nil? then @log.error "Failed to read manifest file : #{file_path}" return nil end - @log.info "Added information for \"#{pkg_name}\" package.. OK" - return pkg_hash[pkg_name] + @log.info "Read information for \"#{pkg_name}\" package.. OK" + return pkg end - private + # get the lastest snapshot # from_server : if true, update from server - def create_remote_pkg_hash(from_server) - - for os in SUPPORTED_OS - filename = PKG_LIST_FILE_PREFIX + os - file_url = @server_addr + "/" + filename - local_file_path = File.join(CONFIG_PATH, filename) - if from_server then - if not FileDownLoader.download(file_url, CONFIG_PATH) then - return false - end - end - local_file_path = File.join(CONFIG_PATH, filename) - if File.exist? local_file_path then - pkg_hash = Parser.read_pkg_list(local_file_path) - @pkg_hash_os[os] = pkg_hash - end + def get_lastest_snapshot(from_server) + ssinfo_file = "snapshot.info" + file_url = File.join(@server_addr, ssinfo_file) + if from_server then + if not FileDownLoader.download(file_url, @config_dist_path, @log) then + @log.warn "Server does not have \"#{ssinfo_file}\" file. This error can be ignored." + end + else + if File.exist? file_url then FileUtils.cp(file_url, @config_dist_path) + else @log.warn "Server does not have \"#{ssinfo_file}\" file. This error can be ignored." end + end + + file_path = File.join(@config_dist_path, ssinfo_file) + if not File.exist? file_path then return nil end + + contents = File.open(file_path, "r").read + + _list = contents.split("\n\n") + if _list.nil? or _list == "" or _list.empty? then return nil end + list = _list[-1].split("\n") + if list.nil? or list == "" or list.empty? then return nil end + _path = list[-1].split(":") + if _path.nil? or _path == "" or _path.length != 2 then return nil end + path = _path[1].strip + if path == nil or path == "" then return nil end + + return path + end + + def get_pkglist_path() + return File.join(@config_dist_path, @snapshot_path) + end + + # if url includes snapshot infomation, retuen true + def is_snapshot_url(addr = nil) + if addr.nil? then addr = @server_addr end + addr_arr = addr.split('/') + if addr_arr[-2].eql? "snapshots" then + return true + else + return false end + end - filename = "archive_pkg_list" - file_url = @server_addr + "/" + filename - if from_server then - if not FileDownLoader.download(file_url, CONFIG_PATH) then - @log.warn "Server does not have \"#{filename}\" file. This error can be ignored." - end + def split_addr_and_snapshot(addr = nil) + if addr.nil? then addr = @server_addr end + addr_arr = addr.split('/') + if addr_arr[-2].eql? "snapshots" then + return addr_arr[0..-3].join("/"), addr_arr[-2..-1].join("/") + else + return nil end - local_file_path = File.join(CONFIG_PATH, filename) + end + + def is_snapshot_exist(ss_path = nil) + if ss_path.nil? then ss_path = @snapshot_path + elsif ss_path == "" then return false end + + local_snapshot_path = File.join(@config_dist_path, ss_path) + if File.directory? local_snapshot_path then return true + else return false end + end + + def read_remote_pkg_list(list_path) + @support_os_list.each do |os| + filename = PKG_LIST_FILE_PREFIX + os + local_file_path = File.join(list_path, filename) + if File.exist? local_file_path then + begin + pkg_hash = Parser.read_repo_pkg_list_from local_file_path + @pkg_hash_os[os] = pkg_hash + @log.info "Get package information for #{os}.. OK" + rescue => e + @log.error( e.message, Log::LV_USER) + @pkg_hash_os[os] = {} + end + else + @log.warn "Failed to read pkg_list_#{os} file" + @pkg_hash_os[os] = {} + end + end + end + + def read_supported_os_list(list_path) + local_file_path = File.join(list_path, OS_INFO_FILE) + if File.exist? local_file_path then + File.open(local_file_path, "r") do |f| + f.each_line do |l| + os = l.strip + if @support_os_list.index(os).nil? then @support_os_list.push(os) end + end + end + @log.info "Get supported os infomation.. OK" + else + @log.warn "Failed to get supported os infomation" + end + end + + def download_os_list(from_server, dist = nil) + if dist.nil? then dist = get_pkglist_path end + file_url = File.join(@server_addr, OS_INFO_FILE) + if from_server then + if not FileDownLoader.download(file_url, dist, @log) then return false end + else + if File.exist? file_url then FileUtils.cp(file_url, dist) + else return false end + end + + return true + end + + def read_archive_pkg_list(list_path) + local_file_path = File.join(list_path, ARCHIVE_PKG_LIST_FILE) if File.exist? local_file_path then File.open(local_file_path, "r") do |f| f.each_line do |l| - @archive_pkg_list.push(l.strip) + pkg = l.strip + if @archive_pkg_list.index(pkg).nil? then @archive_pkg_list.push(pkg) end end - end + end + @log.info "Get archive package infomation.. OK" + else + @log.warn "Failed to get archive package infomation" end + end - return true - end - - private - # create installed package hash - def create_installed_pkg_hash() + def download_archive_pkg_list(from_server, dist = nil) + if dist.nil? then dist = get_pkglist_path end + file_url = File.join(@server_addr, @snapshot_path, ARCHIVE_PKG_LIST_FILE) + if from_server then + if not FileDownLoader.download(file_url, dist, @log) then return false end + else + if File.exist? file_url then FileUtils.cp(file_url, dist) + else return false end + end + + return true + end + + def download_pkg_list(from_server, dist = nil) + if dist.nil? then dist = get_pkglist_path end + @support_os_list.each do |os| + filename = PKG_LIST_FILE_PREFIX + os + file_url = File.join(@server_addr, @snapshot_path, filename) + if from_server then + if not FileDownLoader.download(file_url, dist, @log) then return false end + else + if File.exist? file_url then FileUtils.cp(file_url, dist) + else return false end + end + end + + return true + end + + private + # create installed package hash + def read_installed_pkg_list() config_path = File.join(@location, PACKAGE_INFO_DIR) if not File.directory? config_path then return end installed_pkg_hash_key = get_installed_pkg_list_file_path() if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then return - else + else file_path = installed_pkg_hash_key if not File.exist? file_path then #raise RuntimeError, "#{file_path} file does not exist" return end - pkg_hash = Parser.read_pkg_list(file_path) - @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - end - end + begin + pkg_hash = Parser.read_repo_pkg_list_from file_path + rescue => e + @log.error( e.message, Log::LV_USER) + return + end + @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash + end + end private # check to exist installed package list file @@ -1463,11 +1698,9 @@ class Client end if not pkg_hash.nil? then config_path = File.join(@location, PACKAGE_INFO_DIR) - FileUtils.mkdir_p "#{config_path}" + if not File.exist? config_path then FileUtils.mkdir_p "#{config_path}" end if File.exist? file_path then File.delete(file_path) end File.open(file_path, "a+") do |file| - file.puts "ORIGIN : #{@server_addr}" - file.puts "\n" pkg_list = pkg_hash.values pkg_list.each do |pkg| pkg.print_to_file(file) diff --git a/src/pkg_server/clientOptParser.rb b/src/pkg_server/clientOptParser.rb index ae12c36..695bd43 100644 --- a/src/pkg_server/clientOptParser.rb +++ b/src/pkg_server/clientOptParser.rb @@ -36,8 +36,7 @@ def set_default( options ) if options[:v].nil? then options[:v] = false end end -def option_error_check( options ) - $log.info "option error check" +def option_error_check( options ) case options[:cmd] @@ -51,167 +50,151 @@ def option_error_check( options ) when "download" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli download -p [-o ] [-l ] [-u ] [-t]" - end - - when "upload" then - if options[:alias].nil? or options[:alias].empty? or\ - options[:id].nil? or options[:id].empty? or \ - options[:srcpkg].nil? or options[:srcpkg].empty? then - raise ArgumentError, "Usage: pkg-cli upload -a -i -s [-b ]" - end - - when "source" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli source -p [-o ] [-l ] [-u ]" + raise ArgumentError, "Usage: pkg-cli download -P [-o ] [-l ] [-u ] [--trace]" end when "install" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli install -p [-o ] [-l ] [-u ] [-t] [-f]" + raise ArgumentError, "Usage: pkg-cli install -P [-o ] [-l ] [-u ] [--trace] [--force]" end when "install-file" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli install-lpkg -p [-l ] [-f]" + raise ArgumentError, "Usage: pkg-cli install-lpkg -P [-l ] [-u ] [--trace] [--force]" end when "uninstall" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli uninstall -p [-l ] [-t]" + raise ArgumentError, "Usage: pkg-cli uninstall -P [-l ] [--trace]" end when "show-rpkg" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli show-rpkg -p [-o ] [-u ]" + raise ArgumentError, "Usage: pkg-cli show-rpkg -P [-o ] [-u ]" end when "list-rpkg" then when "show-lpkg" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli show-lpkg -p [-l ]" + raise ArgumentError, "Usage: pkg-cli show-lpkg -P [-l ]" end when "list-lpkg" then when "build-dep" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli build-dep -p [-o ]" + raise ArgumentError, "Usage: pkg-cli build-dep -P [-o ]" end when "install-dep" then if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli install-dep -p [-o ]" + raise ArgumentError, "Usage: pkg-cli install-dep -P [-o ]" end else - raise ArgumentError, "input option incorrect : #{options[:cmd]}" + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" end end def option_parse options = {} - optparse = OptionParser.new do|opts| - # Set a banner, displayed at the top - # of the help screen. - opts.banner = "Usage: pkg-cli {update|clean|download|source|install|uninstall|upgrade|rpkg-show|rpkg-list|lpkg-show|lpkg-list|build-dep|install-dep|help} ..." + "\n" \ - + "\t" + "pkg-cli update [-u ]" + "\n" \ - + "\t" + "pkg-cli clean [-l ] [-f]" + "\n" \ - + "\t" + "pkg-cli download -p [-o ] [-l ] [-u ] [-t]" + "\n" \ - + "\t" + "pkg-cli upload -a -i -s [-b [-o ] [-l ] [-u ]" + "\n" \ - + "\t" + "pkg-cli install -p [-o ] [-l ] [-u ] [-t] [-f]" + "\n" \ - + "\t" + "pkg-cli install-file -p [-l ] [-f]" + "\n" \ - + "\t" + "pkg-cli uninstall -p [-l ] [-t]" + "\n" \ - + "\t" + "pkg-cli upgrade [-l ] [-o ] [-u ] [-t]" + "\n" \ + banner = "Requiest service to package-server and control packages service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-cli [OPTS] or pkg-cli (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "update Update to the latest package in your SDK environment." + "\n" \ + + "\t" + "clean Delete the package in your SDK environment." + "\n" \ + + "\t" + "download Download the package in your SDK environment." + "\n" \ + + "\t" + "install Download the package from package-server and install the package in your SDK environment." + "\n" \ + + "\t" + "install-file Install the package in your SDK environment." + "\n" \ + + "\t" + "uninstall Uninstall the package in your SDK environment." + "\n" \ + + "\t" + "upgrade Upgrade your SDK environment." + "\n" \ + + "\t" + "check-upgrade Check packages to upgrade." + "\n" \ + + "\t" + "show-rpkg Show the package in the package-server." + "\n" \ + + "\t" + "list-rpkg Show the all packages in the package-server." + "\n" \ + + "\t" + "show-lpkg Show the package in your SDK environment." + "\n" \ + + "\t" + "list-lpkg Show the all packages in your SDK environment." + "\n" \ + + "\t" + "build-dep Show build-dependency packages" + "\n" \ + + "\t" + "install-dep Show install-dependency packages" + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + "pkg-cli update [-u ]" + "\n" \ + + "\t" + "pkg-cli clean [-l ] [--force]" + "\n" \ + + "\t" + "pkg-cli download -P [-o ] [-l ] [-u ] [--trace]" + "\n" \ + + "\t" + "pkg-cli install -P [-o ] [-l ] [-u ] [--trace] [--force]" + "\n" \ + + "\t" + "pkg-cli install-file -P [-l ] [-u ] [--trace] [--force]" + "\n" \ + + "\t" + "pkg-cli uninstall -P [-l ] [--trace]" + "\n" \ + + "\t" + "pkg-cli upgrade [-l ] [-o ] [-u ] [--trace]" + "\n" \ + "\t" + "pkg-cli check-upgrade [-l ] [-o ] [-u ]" + "\n" \ - + "\t" + "pkg-cli show-rpkg -p [-o ] [-u ]" + "\n" \ + + "\t" + "pkg-cli show-rpkg -P [-o ] [-u ]" + "\n" \ + "\t" + "pkg-cli list-rpkg [-o ] [-u ]" + "\n" \ - + "\t" + "pkg-cli show-lpkg -p [-l ]" + "\n" \ + + "\t" + "pkg-cli show-lpkg -P [-l ]" + "\n" \ + "\t" + "pkg-cli list-lpkg [-l ]" + "\n" \ - + "\t" + "pkg-cli build-dep -p [-o ]" + "\n" \ - + "\t" + "pkg-cli install-dep -p [-o ]" + "\n" \ + + "\t" + "pkg-cli build-dep -P [-o ]" + "\n" \ + + "\t" + "pkg-cli install-dep -P [-o ]" + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + # Set a banner, displayed at the top + # of the help screen. + + opts.banner = banner - opts.on( '-p', '--pkg ', 'package name or package file name' ) do |name| + opts.on( '-P', '--pkg ', 'package name or package file name' ) do |name| options[:pkg] = name end - opts.on( '-o', '--os ', 'target operating system' ) do |os| + opts.on( '-o', '--os ', 'target operating system: ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64' ) do |os| options[:os] = os end - opts.on( '-u', '--url ', 'package server url' ) do|url| + opts.on( '-u', '--url ', 'package server url: http://127.0.0.1/dibs/unstable' ) do |url| options[:url] = url end - opts.on( '-a', '--alias ', 'ssh alias' ) do|al| - options[:alias] = al - end - - opts.on( '-i', '--id ', 'id' ) do|id| - options[:id] = id - end - - opts.on( '-l', '--loc ', 'location' ) do |loc| - options[:loc] = loc + opts.on( '-l', '--loc ', 'install/download location' ) do |loc| + options[:loc] = loc end - opts.on( '-s', '--src ', 'source package path' ) do|src| - options[:srcpkg] = [] - list = src.tr(" \t","").split(",") - list.each do |l| - if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:srcpkg].push l - end - end - - opts.on( '-t', '--trace', 'enable trace dependent packages' ) do + opts.on( '--trace', 'enable trace dependent packages' ) do options[:t] = true end - opts.on( '-b', '--bin ', 'binary package path' ) do|bin| - options[:binpkg] = [] - list = bin.tr(" \t","").split(",") - list.each do |l| - if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:binpkg].push l - end - end - - opts.on( '-f', '--force', 'enable force' ) do + opts.on( '--force', 'enable force' ) do options[:f] = true end - opts.on( '-h', '--help', 'display this information' ) do + opts.on( '-h', '--help', 'display help' ) do puts opts + exit + end + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() exit end - end - - $log.info "option parsing start" - $log.info "option is : " + ARGV * "," cmd = ARGV[0] - if cmd.eql? "update" or cmd.eql? "download" or \ - cmd.eql? "install" or cmd.eql? "show-rpkg" or \ - cmd.eql? "list-rpkg" or cmd.eql? "source" or \ - cmd.eql? "uninstall" or cmd.eql? "show-lpkg" or \ - cmd.eql? "list-lpkg" or cmd.eql? "upload" or \ - cmd.eql? "install-file" or cmd.eql? "clean" or \ - cmd.eql? "upgrade" or cmd.eql? "check-upgrade" or \ - cmd.eql? "build-dep" or cmd.eql? "install-dep" or \ + if cmd.eql? "update" or cmd.eql? "download" or + cmd.eql? "install" or cmd.eql? "show-rpkg" or + cmd.eql? "list-rpkg" or + cmd.eql? "uninstall" or cmd.eql? "show-lpkg" or + cmd.eql? "list-lpkg" or + cmd.eql? "install-file" or cmd.eql? "clean" or + cmd.eql? "upgrade" or cmd.eql? "check-upgrade" or + cmd.eql? "build-dep" or cmd.eql? "install-dep" or + cmd =~ /(-v)|(--version)/ or cmd =~ /(help)|(-h)|(--help)/ then - if cmd.eql? "help" then ARGV[0] = "-h" end + + if cmd.eql? "help" then + V[0] = "-h" + end options[:cmd] = ARGV[0] else - raise ArgumentError, "first paramter must be {update|clean|download|upload|source|install|install-file|uninstall|upgrade|check-upgrade|show-rpkg|list-rpkg|show-lpkg|list-lpkg|build-dep|install-dep|help} : your input is #{ARGV[0]}" + raise ArgumentError, "Usage: pkg-cli [OPTS] or pkg-cli -h" end optparse.parse! - - $log.info "option parsing end" set_default options diff --git a/src/pkg_server/distribution.rb b/src/pkg_server/distribution.rb index 535f99e..d386890 100644 --- a/src/pkg_server/distribution.rb +++ b/src/pkg_server/distribution.rb @@ -29,14 +29,17 @@ Contributors: require 'fileutils' $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "parser" +require "installer" class Distribution - attr_accessor :name, :location, :server_url + attr_accessor :name, :location, :server_url, :lock_file_path # constant - SUPPORTED_OS = ["linux", "windows", "darwin"] PKG_LIST_FILE_PREFIX = "pkg_list_" - ARCHIVE_PKG_LIST = "archive_pkg_list" + ARCHIVE_PKG_FILE = "archive_pkg_list" + OS_INFO_FILE = "os_info" + SNAPSHOT_INFO_FILE = "snapshot.info" + LOCK_FILE = ".lock_file" def initialize( name, location, server_url, pkg_server ) @@ -45,31 +48,32 @@ class Distribution @server_url = server_url @log = pkg_server.log @integrity = pkg_server.integrity + @lock_file_path = "#{location}/#{LOCK_FILE}" + @pkg_hash_os = {} + @archive_pkg_list = [] + @snapshot_hash = [] + @support_os_list = [] @log.info "Distribution class[#{name}] initialize " - @pkg_hash_os = {} - for os in SUPPORTED_OS - if @location.empty? or ( not File.exist? "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) then - @pkg_hash_os[os] = {} - else - @pkg_hash_os[os] = Parser.read_pkg_list( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - end - end + initialize_pkg_list() end - def register (file_path, pkg) - @log.info "Distribution class's register" + def register (file_path, pkg, internal_flag) if pkg.nil? then raise RuntimeError, "package file does not contain pkginfo.manifest: [#{file_path}]" end + if not @pkg_hash_os.has_key?(pkg.os) then + raise RuntimeError, "package server does not support package's os : [#{pkg.os}]" + end + exist_pkg = @pkg_hash_os[pkg.os][pkg.package_name] # version check and if existing version is higher then upload version? - if not exist_pkg.nil? - if not ( Utils.compare_version( exist_pkg.version, pkg.version ).eql? 1 ) then - raise RuntimeError, "existing package's version is higher than register package" + if (not exist_pkg.nil?) and (not internal_flag) then + if not ( Utils.compare_version( exist_pkg.version, pkg.version ) == 1 ) then + raise RuntimeError, "existing package's version is higher then register package : [#{pkg.package_name}] in [#{pkg.os}]" end end @@ -77,11 +81,16 @@ class Distribution pkg.origin = "local" pkg.source = "" pkg.path = "/binary/" + File.basename( file_path ) - # TODO: windows and mac : sha256sum - if Utils::HOST_OS.eql? "linux" then - pkg.checksum = `sha256sum #{file_path}`.split(" ")[0] - end - pkg.size = `du -b #{file_path}`.split[0].strip + if pkg.checksum.empty? then + # TODO: windows and mac : sha256sum + if Utils.is_unix_like_os( Utils::HOST_OS ) then + pkg.checksum = `sha256sum #{file_path}`.split(" ")[0] + end + end + + if pkg.size.empty? then + pkg.size = `du -b #{file_path}`.split[0].strip + end @pkg_hash_os[pkg.os][pkg.package_name] = pkg @@ -89,7 +98,6 @@ class Distribution end def register_for_test (file_path, pkg) - @log.info "Distribution class's register for test" if pkg.nil? then raise RuntimeError, "package file does not contain pkginfo.manifest: [#{file_path}]" end @@ -99,7 +107,7 @@ class Distribution pkg.source = "" pkg.path = "/temp/" + File.basename( file_path ) # TODO: windows and mac : sha256sum - if Utils::HOST_OS.eql? "linux" then + if Utils.is_unix_like_os( Utils::HOST_OS ) then pkg.checksum = `sha256sum #{file_path}`.split(" ")[0] end pkg.size = `du -b #{file_path}`.split[0].strip @@ -107,8 +115,15 @@ class Distribution return pkg end - def generate_snapshot (name, base_snapshot, append_pkg_list) - @log.info "Distribution class's generate snapshot" + def register_archive_pkg( archive_pkg ) + if not @archive_pkg_list.include? archive_pkg then + @archive_pkg_list.push archive_pkg + else + @log.error("archive package already exist : [#{archive_pkg}]", Log::LV_USER) + end + end + + def generate_snapshot(name, base_snapshot, from_cmd) # if name is nil or empty then create uniq name if name.nil? or name.empty? then name = Utils.create_uniq_name @@ -119,294 +134,372 @@ class Distribution raise "Snapshot is already exist: #{name}" end - if base_snapshot.nil? then base_snapshot = "" else base_snapshot.strip! end - if append_pkg_list.nil? then append_pkg_list = [] end + FileUtils.mkdir "#{@location}/snapshots/#{name}" + + # base_snapshot_path + if base_snapshot.empty? then + snapshot_path = @location + else + snapshot_path = "#{@location}/snapshots/#{base_snapshot.strip}" + end - if base_snapshot.empty? and append_pkg_list.empty? then - FileUtils.mkdir "#{@location}/snapshots/#{name}" + # copy package list + @support_os_list.each do |os| + FileUtils.copy_file( "#{snapshot_path}/#{PKG_LIST_FILE_PREFIX}#{os}", + "#{@location}/snapshots/#{name}/#{PKG_LIST_FILE_PREFIX}#{os}" ) + end + + # copy archive package list + FileUtils.copy_file( "#{snapshot_path}/#{ARCHIVE_PKG_FILE}", + "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_FILE}" ) + + # copy os info file + FileUtils.copy_file( "#{snapshot_path}/#{OS_INFO_FILE}", + "#{@location}/snapshots/#{name}/#{OS_INFO_FILE}" ) - for os in SUPPORTED_OS - FileUtils.copy( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", - "#{@location}/snapshots/#{name}/#{PKG_LIST_FILE_PREFIX}#{os}" ) + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name + + if File.exist? tmp_file_name then + tmp_file_name = "" end + end - # copy archive package list - begin - FileUtils.copy( "#{@location}/#{ARCHIVE_PKG_LIST}", "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_LIST}" ) - rescue => e - @log.warn "ARCHIVE_PKG_LIST not exist" + FileUtils.copy_file( "#{@location}/#{SNAPSHOT_INFO_FILE}", tmp_file_name ) + File.open( tmp_file_name, "a" ) do |f| + f.puts "name : #{name}" + f.puts "time : #{Time.now.strftime("%Y%m%d%H%M%S")}" + if from_cmd then + f.puts "type : manual" + else + f.puts "type : auto" end + f.puts "path : /snapshots/#{name}" + f.puts + end + FileUtils.mv( tmp_file_name, "#{@location}/#{SNAPSHOT_INFO_FILE}", :force => true ) + + # snapshot is generated + @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) + return name + end + def sync(force) + pkg_list_update_flag = false + archive_update_flag = false + distribution_update_flag = false - @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) - # base_snapshot is exist - elsif not ( base_snapshot.empty? ) then - FileUtils.mkdir "#{@location}/snapshots/#{name}" - - for os in SUPPORTED_OS - # check base snapshot exist - if (not File.exist? "#{@location}/snapshots/#{base_snapshot}/#{PKG_LIST_FILE_PREFIX}#{os}") then - raise RuntimeError, "Can't find base snapshot [#{base_snapshot}]" - end - - base_pkg_list = Parser.read_pkg_list( "#{@location}/snapshots/#{base_snapshot}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - snapshot_generate2( name, os, base_pkg_list, append_pkg_list ) + # reload pkg list from newest pkg list file + reload_distribution_information() + + # check distribution's server_url + if @server_url.empty? then + @log.error("This distribution has not remote server", Log::LV_USER) + return false + end + + # generate client class + client = Client.new( @server_url, "#{@location}/binary", @log ) + + # update os list + add_os_list = client.support_os_list - @support_os_list + add_os_list.each do |os| + add_os(os) + pkg_list_update_flag = true + end + + if force then + remove_os_list = @support_os_list - client.support_os_list + remove_os_list.each do |os| + remove_os(os) + pkg_list_update_flag = true end - - # copy archive package list - begin - FileUtils.copy( "#{@location}/#{ARCHIVE_PKG_LIST}", "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_LIST}" ) - rescue => e - @log.warn "ARCHIVE_PKG_LIST not exist" + end + update_pkg_list = [] + + @support_os_list.each do |os| + # error check + if client.pkg_hash_os[os].nil? then + @log.error("package server does not have os : #{os}", Log::LV_USER) + next + end + + server_pkg_name_list = client.pkg_hash_os[os].keys + local_pkg_name_list = @pkg_hash_os[os].keys + full_pkg_name_list = server_pkg_name_list + local_pkg_name_list + + full_pkg_name_list.each do |pkg_name| + ret = sync_package( pkg_name, client, os, force ) + if not ret.nil? then + update_pkg_list.push(ret) + pkg_list_update_flag = true + end end + end + + # sync archive package + update_archive_list = sync_archive_pkg() - @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) - # base_snapshot is empty - else - FileUtils.mkdir "#{@location}/snapshots/#{name}" + # lock + lock_file = Utils.file_lock(@lock_file_path) - for os in SUPPORTED_OS - base_pkg_list = Parser.read_pkg_list( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - snapshot_generate2( name, os, base_pkg_list, append_pkg_list ) - end + # reload pkg list from newest pkg list file + reload_distribution_information() - # copy archive package list - begin - FileUtils.copy( "#{@location}/#{ARCHIVE_PKG_LIST}", "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_LIST}" ) - rescue => e - @log.warn "ARCHIVE_PKG_LIST not exist" + # update pkg_list hash + update_pkg_list.each do |update_option, os, pkg| + # if updated package's os is removed then skip update + if not @support_os_list.include? os then + next end - @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) - end - end + case update_option + when "ADD" + local_pkg = @pkg_hash_os[os][pkg.package_name] + + if (not force) and (not local_pkg.nil?) then + # if updated package 'local' package then skip + if local_pkg.origin.eql? "local" then + next + end - def snapshot_generate2( name, os, pkg_list, append_pkg_list ) - @log.info "snapshot_generate2: input append_pkg_list #{append_pkg_list}" - append_pkg_list.each do |pkg| - # os check - if pkg.os.eql? os.strip then pkg_list[pkg.package_name] = pkg end + # if package is update when sync time then skip + if Utils.compare_version(local_pkg.version, pkg.version) == -1 then + next + end + end + + @pkg_hash_os[os][pkg.package_name] = pkg + when "REMOVE" + if not force then + if @pkg_hash_os[os][pkg.package_name].origin.eql? "local" then + next + end + end + + @pkg_hash_os[os].delete(pkg.package_name) + else + @log.error("Unsupportd update option : #{update_option}", Log::LV_USER) + next + end end - - File.open( "#{@location}/snapshots/#{name}/#{PKG_LIST_FILE_PREFIX}#{os}", "w" ) do |f| - pkg_list.each_value do |pkg| - pkg.print_to_file(f) - f.puts + + update_archive_list.each do |pkg| + if not @archive_pkg_list.include? pkg then + @archive_pkg_list.push pkg + archive_update_flag = true end - end - end + end - def sync( force, os ) + # update pkg_list file + if pkg_list_update_flag then + write_all_pkg_list() + distribution_update_flag = true + end - # check distribution's server_url - if @server_url.empty? then - @log.error( "This distribution has not remote server" , Log::LV_USER) - return + # update archive list file + if archive_update_flag then + write_archive_pkg_list() + distribution_update_flag = true end - # generate client class - client_bin = Client.new( @server_url, "#{@location}/binary", @log ) - client_bin.update - client_src = Client.new( @server_url, "#{@location}/source", @log ) - client_src.update - - source_pkg_path_list = [] - dep_pkg_path_list = [] - - # error check - if client_bin.pkg_hash_os[os].nil? then - raise "Package list can't generated. url is #{@server_url}. os is #{os}" - end + # unlock + Utils.file_unlock(lock_file) - # check existing source package list - @pkg_hash_os[os].each_value do |pkg| - if not source_pkg_path_list.include? pkg.src_path then - source_pkg_path_list.push pkg.src_path - end - end + return distribution_update_flag + end - full_pkg_list = client_bin.pkg_hash_os[os].merge(@pkg_hash_os[os]) + def add_os(os) + if @support_os_list.include? os then + @log.error("#{os} is already exist ", Log::LV_USER) + return + end - full_pkg_list.each_key do |pkg_name| - server_pkg = client_bin.pkg_hash_os[os][pkg_name] - local_pkg = @pkg_hash_os[os][pkg_name] + # update os information + @support_os_list.push os + @pkg_hash_os[os] = {} + File.open("#{@location}/#{OS_INFO_FILE}", "a") do |f| + f.puts os + end - # if server and local has package - if ( not server_pkg.nil? ) and ( not local_pkg.nil? ) then - # if server version is not updated then skip - if ( Utils.compare_version( local_pkg.version, server_pkg.version ).eql? 0 ) then - @log.info "existing packages version equal to server's version. so package[#{pkg_name}] skip" - - next - end + # create pkg_list_#{os} file + File.open( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", "w" ) do |f| end + end - # if server's pakcage is local package and mode is not force then local package will be upaded - if ( local_pkg.origin.eql? "local" ) and ( not force ) then - @log.info "package [#{pkg_name}] is local package. so skip update" - - next - end + def clean( remain_snapshot_list ) + file_list = [] + used_archive_list = [] - # package update - @log.info "update package from server: [#{pkg_name}]" - file_path_list = client_bin.download( pkg_name, os, false ) - - # file download error check - if file_path_list.nil? or file_path_list.empty? then - @log.error( "Can't download package file #{pkg_name}" , Log::LV_USER) - next - else - @log.info "download binary package successfully: [#{pkg_name}]" - file_path = file_path_list[0] - end - - # update pkg class - server_pkg.path = "/binary/#{File.basename(file_path)}" - server_pkg.origin = client_bin.server_addr - @pkg_hash_os[os][pkg_name] = server_pkg - - dep_pkg_path_list = dep_pkg_path_list + server_pkg.source_dep_list - - # if binary only package, then skip downloading its source - if server_pkg.src_path.empty? then next end - - # if binary's source package is not downlaoded, download it - if ( not source_pkg_path_list.include? server_pkg.src_path ) then - @log.info "download source package: [#{server_pkg.src_path}]" - file = client_src.download_source( pkg_name, os ) - if file.nil? then - @log.error "Can't download source package [#{pkg_name}]" + # collect remaining file's name from current package server version + @support_os_list.each do |os| + @pkg_hash_os[os].each_value{ |pkg| + file_list.push(pkg.path.sub("/binary/","")) + + pkg.source_dep_list.each do |source_dep| + if @archive_pkg_list.include? source_dep.package_name then + used_archive_list.push source_dep.package_name else - source_pkg_path_list.push server_pkg.src_path - end - end - # if package exist only server - elsif ( not server_pkg.nil? ) then - #downnlaod binary package - file_path_list = client_bin.download( pkg_name, os, false ) - - # file download error check - if file_path_list.nil? or file_path_list.empty? then - @log.error( "Can't download package file #{pkg_name}", Log::LV_USER) - next - else - @log.info "download binary package successfully: [#{pkg_name}]" - file_path = file_path_list[0] - end - - # update pkg class - server_pkg.path = "/binary/#{File.basename(file_path)}" - server_pkg.origin = client_bin.server_addr - @pkg_hash_os[os][pkg_name] = server_pkg - - dep_pkg_path_list = dep_pkg_path_list + server_pkg.source_dep_list - - # if binary only package, then skip downloading its source - if server_pkg.src_path.empty? then next end - - # if binary's source package is not downlaoded, download it - if not source_pkg_path_list.include? server_pkg.src_path then - @log.info "download source package: [#{server_pkg.src_path}]" - file = client_src.download_source( pkg_name, os ) - if file.nil? - @log.error "Can't download source package [#{server_pkg.src_path}]" - else - source_pkg_path_list.push server_pkg.src_path + @log.error("Can't find dependency source package : #{source_dep.package_name}") end end - # if package exist only local - elsif ( not local_pkg.nil? ) then - # if pakcage is not local package then server's package is removed - # so, local package remove - if not local_pkg.origin.eql? "local" then - @pkg_hash_os[os].delete(pkg_name) + } + end + + # remain only used archive package + @archive_pkg_list = used_archive_list.uniq + write_archive_pkg_list + + # collect remaning file's name from snapshot list + remain_snapshot_list.each do |snapshot| + os_info = "#{@location}/snapshots/#{snapshot}/#{OS_INFO_FILE}" + os_list = [] + # if snapshot has os_info file then using that file + if File.exist? os_info + File.open( os_info, "r" ) do |f| + f.each_line do |l| + os_list.push l.strip + end end + # if snapshot does not have os_info file then using package server os_info list else - raise RuntimeError,"hash merge error!" + os_list = @support_os_list end - end - @log.info "pkg file update end" - # download dependency source packages - dep_pkg_path_list.uniq.each do |dep| - if dep.package_name.strip.empty? then next end - @log.info "download dep package: [#{dep.package_name}]" - file = client_src.download_dep_source( dep.package_name ) - if file.nil? - @log.error "Can't download dep package [#{dep.package_name}]" - end + os_list.each do |os| + begin + info_file = "#{@location}/snapshots/#{snapshot}/#{PKG_LIST_FILE_PREFIX}#{os}" + if not File.exist? info_file then + @log.error( "pkg list file does not exist : #{info_file}", Log::LV_USER) + next + end + + pkg_list = Parser.read_repo_pkg_list_from(info_file) + + pkg_list.each_value{ |pkg| + file_list.push(pkg.path.sub("/binary/","")) + } + rescue => e + @log.error( e.message, Log::LV_USER) + end + end + + used_archive_list = used_archive_list + read_archive_pkg_list( snapshot ) end - @log.info "pkg deb file update end" - # pakcage list file update - write_pkg_list(os) - @log.info "write pkg list" - end + file_list.uniq! + used_archive_list.uniq! - def sync_archive_pkg - client = Client.new( @server_url, "#{@location}/source", @log ) - client.update - - downloaded_list = [] - client.archive_pkg_list.each do |pkg| - if not File.exist? "#{@location}/source/#{pkg}" then - file = client.download_dep_source(pkg) - if file.nil? - @log.error "Can't download archive package [#{file}]" - else - downloaded_list.push pkg - end + # remove unused binary file + Dir.new( @location + "/binary" ).each do |file| + if file.start_with? "." then next end + + if not file_list.include? file then + FileUtils.rm "#{@location}/binary/#{file}" end end - write_archive_pkg_list( downloaded_list ) - end + # remove unused archive file + Dir.new( @location + "/source" ).each do |file| + if file.start_with? "." then next end + + if not used_archive_list.include? file then + FileUtils.rm "#{@location}/source/#{file}" + end + end + + # remove unused snapshot + Dir.new( @location + "/snapshots" ).each do |snapshot| + if snapshot.start_with? "." then next end + + if not remain_snapshot_list.include? snapshot then + FileUtils.rm_rf "#{@location}/snapshots/#{snapshot}" + end + end + + # upate snapshot.info file + update_snapshot_info_file(remain_snapshot_list) + end + + def write_all_pkg_list + @support_os_list.each do |os| + write_pkg_list(os) + end + end def write_pkg_list( os ) - File.open( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", "w" ) do |f| + # if input os is empty then return + if os.nil? or os.empty? then return end + + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name + + if File.exist? tmp_file_name then + tmp_file_name = "" + end + end + + File.open( tmp_file_name, "w" ) do |f| @pkg_hash_os[os].each_value do |pkg| + # insert package information to file pkg.print_to_file(f) + # insert empty line to file f.puts end end - end - def write_archive_pkg_list( pkg_file_name_list ) - File.open( "#{@location}/#{ARCHIVE_PKG_LIST}", "a" ) do |f| - pkg_file_name_list.map { |name| f.puts(name) } - end + FileUtils.mv( tmp_file_name, "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", :force => true ) end # input: package file path(zip file) # return: pkg def get_package_from_file(file_path) - tmp_dir = "./" + Utils.create_uniq_name - FileUtils.mkdir "#{@location}/#{tmp_dir}" - - # file extention is zip - if file_path.end_with? ".zip" then - system("unzip -q #{file_path} pkginfo.manifest -d #{@location}/#{tmp_dir} ") - # file extention is tar.gz - elsif file_path.end_with? ".tar.gz" or file_path.end_with? ".tar" then - system("tar -xzf #{file_path} -C #{@location}/#{tmp_dir}") + tmp_dir = @location + "/" + Utils.create_uniq_name + + #if file extension is .zip then check pkginfo.manifest + if File.extname(file_path).eql? ".zip" then + FileUtils.mkdir tmp_dir + + ret = FileInstaller.extract_a_file(file_path, "pkginfo.manifest", tmp_dir, @log) else - raise "unsupported zipping file. just use [zip/tar.gz]" + return nil end - pkg = Parser.read_pkginfo( "#{@location}/#{tmp_dir}/pkginfo.manifest" ) - FileUtils.rm_rf "#{@location}/#{tmp_dir}" - return pkg + # if pkginfo.manifest file exist + if not ret.nil? then + begin + pkg = Parser.read_single_pkginfo_from "#{tmp_dir}/pkginfo.manifest" + rescue => e + @log.error( e.message, Log::LV_USER) + return nil + end + + FileUtils.rm_rf tmp_dir + return pkg + # if pkginfo.manifest file does not exist + else + FileUtils.rm_rf tmp_dir + return nil + end end def remove_pkg( pkg_name_list, os ) - for package_name in pkg_name_list + if os.eql? "all" then os_list = @support_os_list + else os_list = [ os ] + end + + pkg_name_list.each do |package_name| removed_flag = false - if os.eql? "all" then os_list = SUPPORTED_OS - else os_list = [ os ] - end + os_list.each do |os| + if not @support_os_list.include? os then + @log.error( "package server does not support input os : #{os}") + next + end - for os in os_list if @pkg_hash_os[os].key?(package_name) then @log.info( "remove package [#{package_name}] in #{os}", Log::LV_USER) @pkg_hash_os[os].delete(package_name) @@ -415,73 +508,403 @@ class Distribution end if not removed_flag then - @log.error( "Can't find package: #{package_name}", Log::LV_USER) + if @archive_pkg_list.include? package_name then + @archive_pkg_list.delete package_name + else + @log.error( "Can't find package: [#{package_name}]", Log::LV_USER) + end end end - + # check install dependency integrity - check_integrity + if @integrity.eql? "YES" then + @log.info "integrity check" + check_integrity + else + @log.info "skip integrity check" + end - for os in SUPPORTED_OS + + # update pkg_list file + os_list.each do |os| write_pkg_list(os) end + write_archive_pkg_list end + def remove_snapshot( snapshot_list ) + remain_snapshot = [] + removed_snapshot = [] + + # remove unused snapshot + Dir.new( @location + "/snapshots" ).each do |snapshot| + if snapshot.start_with? "." then next end + + if snapshot_list.include? snapshot then + FileUtils.rm_rf "#{@location}/snapshots/#{snapshot}" + snapshot_list.delete snapshot + removed_snapshot.push snapshot + else + remain_snapshot.push snapshot + end + end + + if not snapshot_list.empty? then + @log.output( "snapshot not exist : #{snapshot_list.join(",")}", Log::LV_USER ) + end + + if not removed_snapshot.empty? then + @log.output( "snapshot removed: #{removed_snapshot.join(",")}", Log::LV_USER ) + end + + update_snapshot_info_file(remain_snapshot) + end + def check_integrity @log.info "check server pkg's install dependency integrity" - if not @integrity.eql? "YES" then - @log.info "skip integrity check" - return + @support_os_list.each do |os| + @pkg_hash_os[os].each_value.each do |pkg| + check_package_integrity(pkg) + end end + end - for os in SUPPORTED_OS - for pkg in @pkg_hash_os[os].each_value - error_msg = "[#{pkg.package_name}]'s install dependency not matched in " + def check_package_integrity(pkg) + error_msg = "[[#{pkg.package_name}] in #{pkg.os}]'s install dependency not matched in " + os = pkg.os - for dep in pkg.install_dep_list - if @pkg_hash_os[os].has_key? dep.package_name then - target_pkg = @pkg_hash_os[os][dep.package_name] - else - raise RuntimeError,(error_msg + dep.to_s) - end + pkg.install_dep_list.each do |dep| + if @pkg_hash_os[os].has_key? dep.package_name then + target_pkg = @pkg_hash_os[os][dep.package_name] + else + raise RuntimeError,(error_msg + dep.to_s) + end + + # check package's version + if not dep.match? target_pkg.version then + raise RuntimeError,(error_msg + dep.to_s) + end + + end + + error_msg = "[[#{pkg.package_name}] in #{pkg.os}]'s build dependency not matched in " + pkg.build_dep_list.each do |dep| + if dep.target_os_list.length == 0 then + build_dep_os = os + else + build_dep_os = dep.target_os_list[0] + end + + if @pkg_hash_os[build_dep_os].has_key? dep.package_name then + target_pkg = @pkg_hash_os[build_dep_os][dep.package_name] + else + raise RuntimeError,(error_msg + dep.to_s) + end + + # check package's version + if not dep.match? target_pkg.version then + raise RuntimeError,(error_msg + dep.to_s) + end + end + + error_msg = "[[#{pkg.package_name}] in #{pkg.os}]'s source dependency not matched in " + pkg.source_dep_list.each do |dep| + if not @archive_pkg_list.include? dep.package_name then + raise RuntimeError,(error_msg + dep.to_s) + end + end + end + + def read_archive_pkg_list( snapshot_name ) + pkg_list = [] + + if snapshot_name.empty? + file_name = @location + "/" + ARCHIVE_PKG_FILE + else + file_name = @location + "/snapshots/" + snapshot_name + "/" + ARCHIVE_PKG_FILE + end + + if File.exist? file_name + File.open(file_name, "r") do |f| + f.each_line do |l| + pkg_list.push(l.strip) + end + end + end + + return pkg_list + end + + def write_archive_pkg_list() + File.open( "#{@location}/#{ARCHIVE_PKG_FILE}", "w" ) do |f| + @archive_pkg_list.each do |pkg| + f.puts(pkg) + end + end + end + + def initialize_pkg_list + if not File.exist? "#{@location}/#{OS_INFO_FILE}" then + return + end + + # get support_os_list + @support_os_list = [] + File.open( "#{@location}/#{OS_INFO_FILE}", "r" ) do |f| + f.each_line do |l| + @support_os_list.push l.strip + end + end + + # read package_list file + @support_os_list.each do |os| + @pkg_hash_os[os] = {} + pkg_list_file = "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" + + if File.exist? pkg_list_file then + begin + @pkg_hash_os[os] = Parser.read_repo_pkg_list_from( pkg_list_file ) + rescue => e + @log.error( e.message, Log::LV_USER) + @pkg_hash_os[os] = nil + end + end + end + + # read archive package_list file + @archive_pkg_list = read_archive_pkg_list("") + end + + def get_link_package(pkg, pkg_os) + pkg.os_list.each do |os| + # skip in same os for origin package + if pkg_os.eql? os then next end + # skip in unsupported os + if not @support_os_list.include? os then next end + + exist_pkg = @pkg_hash_os[os][pkg.package_name] + if exist_pkg.nil? then next end + + compare_version = Utils.compare_version(pkg.version, exist_pkg.version) + # if version same then compatible package + if compare_version == 0 then + return exist_pkg + end + end + + return nil + end + + # PRIVATE METHODS/VARIABLES + private + + def sync_package( pkg_name, client, os, force ) + server_pkg = client.pkg_hash_os[os][pkg_name] + local_pkg = @pkg_hash_os[os][pkg_name] + + # if server and local has package + if ( not server_pkg.nil? ) and ( not local_pkg.nil? ) then + version_cmp = Utils.compare_version( local_pkg.version, server_pkg.version ) + if ( version_cmp == 0 ) then + # version is same then skip update + return nil + end + + if ( local_pkg.origin.eql? "local" ) and (not force) then + # local_pkg is generated from local and not force mode then skip update + return nil + end + + pkg = sync_package2( server_pkg, client, os ) + return ["ADD", os, pkg] + # if package exist only server + elsif ( not server_pkg.nil? ) then + pkg = sync_package2( server_pkg, client, os ) + return ["ADD", os, pkg] + # if package exist only local + elsif ( not local_pkg.nil? ) then + # if local pkg is generated from local then skip + if local_pkg.origin.eql? "local" and (not force) then + return nil + end + + # package remove + return ["REMOVE", os, local_pkg] + else + raise RuntimeError,"hash merge error!" + end + + return nil + end + + def sync_package2( pkg, client, os ) + pkg_name = pkg.package_name + + # package update + file_path_list = client.download( pkg_name, os, false ) + + # file download error check + if file_path_list.nil? or file_path_list.empty? then + @log.error("Can't download package file [#{pkg_name}]", Log::LV_USER) + return nil + else + file_path = file_path_list[0] + end - # check package's version - if not dep.match? target_pkg.version then - raise RuntimeError,(error_msg + dep.to_s) - end + # update pkg class + pkg.path = "/binary/#{File.basename(file_path)}" + pkg.origin = client.server_addr + return pkg + + end + + def update_snapshot_info_file(remain_snapshot_list) + if not File.exist? "#{@location}/#{SNAPSHOT_INFO_FILE}" + @log.error "Can not find snapshot info file" + return + end - end + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name - error_msg = "[#{pkg.package_name}]'s build dependency not matched in " - for dep in pkg.build_dep_list - if dep.target_os_list.length == 0 then - build_dep_os = os + if File.exist? tmp_file_name then + tmp_file_name = "" + end + end + + # modify snapshot info File + info_file = File.readlines("#{@location}/#{SNAPSHOT_INFO_FILE}") + File.open(tmp_file_name, 'w') do |f| + save_flag = false + info_file.each { |line| + if line =~ /name :/ then + if remain_snapshot_list.include? line.split(':')[1].strip then + save_flag = true else - build_dep_os = dep.target_os_list[0] + save_flag = false end - if @pkg_hash_os[build_dep_os].has_key? dep.package_name then - target_pkg = @pkg_hash_os[build_dep_os][dep.package_name] - else - raise RuntimeError,(error_msg + dep.to_s) - end - - # check package's version - if not dep.match? target_pkg.version then - raise RuntimeError,(error_msg + dep.to_s) - end - - end - - error_msg = "[#{pkg.package_name}]'s source dependency not matched in " - for dep in pkg.source_dep_list - # check source package exist - if not File.exist? "#{@location}/source/#{dep.package_name}" - raise RuntimeError,(error_msg + dep.to_s) - end - end - end + end + + if save_flag then + f.puts line + end + } + end + + FileUtils.mv( tmp_file_name, "#{@location}/#{SNAPSHOT_INFO_FILE}", :force => true ) + end + + def get_all_reverse_depends_pkgs(pkg, checked_list) + depends_list = [] + + @support_os_list.each do |os| + @pkg_hash_os[os].each_value{ |dpkg| + if dpkg.install_dep_list.include? pkg or \ + dpkg.build_dep_list.include? pkg then + depends_list.push opkg + end + + } + end + + depends_list.each do |dpkg| + checked_list.push dpkg + rdepends_list = get_all_reverse_depends_pkgs( dpkg, checked_list ) + end + + return rdepends_list + end + + def reload_distribution_information + if not File.exist?("#{@location}/#{OS_INFO_FILE}") then + return end + + # get support_os_list + @support_os_list = [] + File.open( "#{@location}/#{OS_INFO_FILE}", "r" ) do |f| + f.each_line do |l| + @support_os_list.push l.strip + end + end + + # read binary package_list file + @support_os_list.each do |os| + @pkg_hash_os[os] = {} + pkg_list_file = "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" + + if File.exist? pkg_list_file then + begin + @pkg_hash_os[os] = Parser.read_repo_pkg_list_from( pkg_list_file ) + rescue => e + @log.error( e.message, Log::LV_USER) + @pkg_hash_os[os] = nil + end + end + end + + # read archive package_list file + @archive_pkg_list = read_archive_pkg_list( "" ) end + + def remove_os(os) + if not @support_os_list.include? os then + @log.error("Can't remove os : #{os} does not exist ", Log::LV_USER) + end + + # update os information + @support_os_list.delete os + @pkg_hash_os.delete os + + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name + + if File.exist? tmp_file_name then + tmp_file_name = "" + end + end + + info_file = File.readlines("#{@location}/#{OS_INFO_FILE}") + File.open(tmp_file_name, "w") do |f| + info_file.each do |line| + if not line.strip.eql? os then + f.puts line + end + end + end + + FileUtils.mv( tmp_file_name, "#{@location}/#{OS_INFO_FILE}", :force => true ) + + # delete pkg_list_#{os} file + File.delete( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) + end + + def sync_archive_pkg + client = Client.new( @server_url, "#{@location}/source", @log ) + + download_list = client.archive_pkg_list - @archive_pkg_list + + updated_file_list = [] + + # if update list is empty then return empty array + if download_list.empty? then return updated_file_list end + + download_list.each do |pkg| + file = client.download_dep_source(pkg) + if file.nil? + @log.error("Can't download archive package [#{pkg}]", Log::LV_USER) + else + updated_file_list.push pkg + end + end + + return updated_file_list + end + end diff --git a/src/pkg_server/downloader.rb b/src/pkg_server/downloader.rb index 0308208..7e5a8c8 100644 --- a/src/pkg_server/downloader.rb +++ b/src/pkg_server/downloader.rb @@ -31,30 +31,25 @@ require "utils" class FileDownLoader - @@log = nil - - def FileDownLoader.set_logger(logger) - @@log = logger - end - - def FileDownLoader.download(url, path) + def FileDownLoader.download(url, path, logger) ret = false if not File.directory? path then - @@log.error "\"#{path}\" does not exist" + logger.error "\"#{path}\" does not exist" return ret - end - + end + is_remote = Utils.is_url_remote(url) filename = url.split('/')[-1] - fullpath = File.join(path, filename) + logger.info "Downloading #{url}" if is_remote then - ret = system "wget #{url} -O #{fullpath} -nv" + ret = Utils.execute_shell_with_log( "wget #{url} -O #{fullpath} -nv", logger ) + #ret = Utils.execute_shell( "wget #{url} -O #{fullpath} -q") else if not File.exist? url then - @@log.error "\"#{url}\" file does not exist" + logger.error "\"#{url}\" file does not exist" return false else ret = system "cp #{url} #{fullpath}" @@ -62,6 +57,12 @@ class FileDownLoader end # need verify + if ret then + logger.info "Downloaded #{filename}.. OK" + else + logger.info "Failed to download #{filename}" + logger.info " [dist: #{path}]" + end return ret end end diff --git a/src/pkg_server/installer.rb b/src/pkg_server/installer.rb index 84ea930..d9582b8 100644 --- a/src/pkg_server/installer.rb +++ b/src/pkg_server/installer.rb @@ -31,22 +31,21 @@ $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "packageServerConfig" require "log" require "utils" +if Utils.is_windows_like_os( Utils::HOST_OS ) then + require "rubygems" + require "zip/zip" +end class FileInstaller CONFIG_PATH = "#{PackageServerConfig::CONFIG_ROOT}/client" PACKAGE_INFO_DIR = ".info" + PACKAGE_MANIFEST = "pkginfo.manifest" - @@log = nil - - def FileInstaller.set_logger(logger) - @@log = logger - end - - def FileInstaller.install(package_name, package_file_path, type, target_path) + def FileInstaller.install(package_name, package_file_path, type, target_path, logger) if not File.exist? package_file_path then - @@log.error "\"#{package_file_path}\" file does not exist." + logger.error "\"#{package_file_path}\" file does not exist." return false end @@ -55,81 +54,153 @@ class FileInstaller when "binary" then uniq_name = Utils.create_uniq_name path = Utils::HOME + "/tmp/#{uniq_name}" - if Utils::HOST_OS.eql? "windows" then + # windows has limitation for file path length + if Utils.is_windows_like_os( Utils::HOST_OS ) then drive = Utils::HOME.split("/")[0] path = "#{drive}/#{uniq_name}" end - FileUtils.mkdir_p "#{path}" - - if File.directory? path then - log = "##### create temporary dir : #{path} #####\n" - else - log = "##### [Failed] create temporary dir : #{path} #####\n" - return false + if not File.exist? path then FileUtils.mkdir_p "#{path}" end + + if File.directory? path then + log = "## create temporary dir : #{path}\n" + else + logger.error "Failed to create temporary dir" + logger.info " [path: #{path}]" + return false + end + + begin + logger.info "Installing \"#{package_name}\" package.." + logger.info " [file: #{package_file_path}]" + + log = log + "## Extract file : #{package_file_path}\n" + result = extract_file(package_name, package_file_path, path, target_path, logger) + if result == "" or result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Move files : \"#{path}\" to \"#{target_path}\"\n" + result = move_dir(package_name, path, target_path, logger) + if result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Execute install script\n" + result = execute_install_script(package_name, path, target_path, logger) + if result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Move remove script\n" + result = move_remove_script(package_name, path, target_path, logger) + if result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Remove temporary dir : #{path} #####\n" + result = Utils.execute_shell_return("rm -rf #{path}") + if result.nil? then + logger.warn "Failed to remove temporary path" + logger.info " [path: #{path}]" + end + rescue Interrupt + logger.error "FileInstaller: Interrupted.." + Utils.execute_shell("rm -rf #{path}") + logger.info "Removed #{path}" + raise Interrupt end - - log = log + "##### extract file : #{package_file_path} #####\n" - log = log + extract_file(package_name, package_file_path, path, target_path) - move_dir(package_name, path, target_path) - - log = log + "##### execute install script #####\n" - log = log + execute_install_script(package_name, path, target_path) - - log = log + "##### move remove script #####\n" - move_remove_script(package_name, path, target_path) - - log = log + "##### remove temporary dir : #{path} #####\n" - Utils.execute_shell("rm -rf #{path}") - + write_log(target_path, package_name, log) +=begin target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - FileUtils.mkdir_p(target_config_path) + if not File.exist? target_config_path then FileUtils.mkdir_p(target_config_path) end pkg_inst_log = "#{package_name}_inst.log" pkg_inst_log_path = File.join(target_config_path, pkg_inst_log) File.open(pkg_inst_log_path, "a+") do |f| f.puts log end - +=end when "source" then end # need verify + logger.info "Installed \"#{package_name}\" package.. OK" + logger.info " [path: #{target_path}]" return true; - end + end - def FileInstaller.move_remove_script(package_name, path, target_path) + def FileInstaller.write_log(target_path, package_name, log) + target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + if not File.exist? target_config_path then FileUtils.mkdir_p(target_config_path) end + pkg_inst_log = "#{package_name}_inst.log" + pkg_inst_log_path = File.join(target_config_path, pkg_inst_log) + + File.open(pkg_inst_log_path, "a+") do |f| + f.puts log + end + end + + def FileInstaller.move_remove_script(package_name, path, target_path, logger) target_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - FileUtils.mkdir_p(target_path) + if not File.exist? target_path then FileUtils.mkdir_p(target_path) end script_file_prefix = "#{path}/remove.*" script_file = Dir.glob(script_file_prefix)[0] + log = "" if not script_file.nil? then - FileUtils.mv(script_file, target_path) - end + result = Utils.execute_shell_return("mv #{script_file} #{target_path}") + if result.nil? then + logger.error "Failed to move a remove script" + logger.info " [file: #{script_file}]" + logger.info " [from: #{path}]" + logger.info " [to: #{target_path}]" + return nil + else log = result.join("") end + logger.info "Moved remove script file.. OK" + log = log + "[file: #{script_file}]\n" + log = log + "[from: #{path}]\n" + log = log + "[to: #{target_path}]\n" + end + + return log end - def FileInstaller.execute_install_script(package_name, path, target_path) + # Does not verify that the script execution is successful. + # Register shortcut should be failed. + def FileInstaller.execute_install_script(package_name, path, target_path, logger) script_file_prefix = "#{path}/install.*" script_file = Dir.glob(script_file_prefix)[0] log = "" if not script_file.nil? then - @@log.info "Execute \"#{script_file}\" file" - if Utils::HOST_OS.eql? "windows" then - cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" + logger.info "Execute \"#{script_file}\" file" + if Utils.is_windows_like_os( Utils::HOST_OS ) then + cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" else cmd = "INSTALLED_PATH=\"#{target_path}\" #{script_file}" - end + end + logger.info " [cmd: #{cmd}]" log = `#{cmd}` - end + logger.info "Executed install script file.. OK" + log = log + "[file: #{script_file}]\n" + log = log + "[cmd: #{cmd}]\n" + end + return log end - def FileInstaller.execute_remove_script(package_name, target_path) + # Does not verify that the script execution is successful. + # Removing shortcut should be failed. + def FileInstaller.execute_remove_script(package_name, target_path, logger) info_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" if not File.directory? info_path then - return false + logger.error "\"#{info_path}\" does not exist." + return nil end script_file_prefix = "#{info_path}/remove.*" @@ -137,21 +208,28 @@ class FileInstaller log = "" if not script_file.nil? then - @@log.info "Execute \"#{script_file}\" file" - if Utils::HOST_OS.eql? "windows" then + logger.info "Execute \"#{script_file}\" file" + if Utils.is_windows_like_os( Utils::HOST_OS ) then cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" else cmd = "INSTALLED_PATH=\"#{target_path}\" #{script_file}" end + logger.info " [cmd: #{cmd}]" log = `#{cmd}` - end + logger.info "Executed remote script file.. OK" + log = log + "[file: #{script_file}]\n" + log = log + "[cmd: #{cmd}]\n" + end + + return log end - def FileInstaller.remove_pkg_files(package_name, target_path) + def FileInstaller.remove_pkg_files(package_name, target_path, logger) list_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" if not File.directory? list_path then - return false + logger.error "\"#{list_path}\" does not exist." + return false end list_file_name = "#{list_path}/#{package_name}.list" @@ -176,17 +254,18 @@ class FileInstaller begin Dir.rmdir(file_path) rescue SystemCallError - @@log.warn "\"#{file_path}\" directory is not empty" + logger.warn "\"#{file_path}\" directory is not empty" end else directories.push(file_path) end elsif File.file? file_path then FileUtils.rm_f(file_path) elsif File.symlink? file_path then File.unlink file_path # if files are already removed by remove script, - else @@log.warn "\"#{file_path}\" does not exist" end + else logger.warn "\"#{file_path}\" does not exist" end end end directories.reverse.each do |path| + if not File.directory? path then next end entries = Dir.entries(path) if entries.include? "." then entries.delete(".") end if entries.include? ".." then entries.delete("..") end @@ -194,40 +273,67 @@ class FileInstaller begin Dir.rmdir(path) rescue SystemCallError - @@log.warn "\"#{file_path}\" directory is not empty" + logger.warn "\"#{file_path}\" directory is not empty" end else next end end end - #FileUtils.rm_rf(list_path) Utils.execute_shell("rm -rf #{list_path}") return true end - def FileInstaller.uninstall(package_name, type, target_path) + def FileInstaller.uninstall(package_name, type, target_path, logger) case type when "binary" then - execute_remove_script(package_name, target_path) - remove_pkg_files(package_name, target_path) + result = execute_remove_script(package_name, target_path, logger) + if result.nil? then return false end + if not remove_pkg_files(package_name, target_path, logger) then return false end when "source" then end return true end - def FileInstaller.move_dir(package_name, source_path, target_path) + def FileInstaller.move_dir(package_name, source_path, target_path, logger) config_path = File.join(target_path, PACKAGE_INFO_DIR, package_name) - FileUtils.cp_r Dir.glob("#{source_path}/data/*"), target_path - FileUtils.cp "#{source_path}/pkginfo.manifest", config_path + pkginfo_path = File.join(source_path, PACKAGE_MANIFEST) + data_path = File.join(source_path, "data") + log = "" + + if not File.exist? pkginfo_path then + logger.error "#{PACKAGE_MANIFEST} file does not exist. Check #{source_path}" + return nil + else FileUtils.cp pkginfo_path, config_path end + + if File.exist? data_path then + # if os is linux, use cpio. it is faster than cp + if Utils.is_linux_like_os( Utils::HOST_OS ) then + absolute_path = `readlink -f #{target_path}` + result = Utils.execute_shell_return("cd #{data_path}; find . -depth | cpio -pldm #{absolute_path}") + else + result = Utils.execute_shell_return("cp -r #{data_path}/* #{target_path}") + end + if result.nil? then + logger.error "Failed to move files" + logger.info " [from: #{source_path}]" + logger.info " [to: #{target_path}]" + return nil + end + logger.info "Moved files.. OK" + log = log + "[from: #{source_path}]\n" + log = log + "[to: #{target_path}]\n" + else logger.warn "\"data\" directory does not exist." end + + return log end - def FileInstaller.extract_file(package_name, package_file_path, path, target_path) + def FileInstaller.extract_file(package_name, package_file_path, path, target_path, logger) dirname = File.dirname(package_file_path) filename = File.basename(package_file_path) ext = File.extname(filename) target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - FileUtils.mkdir_p(target_config_path) + if not File.exist? target_config_path then FileUtils.mkdir_p(target_config_path) end pkg_file_list = "#{package_name}.list" pkg_file_list_path = File.join(target_config_path, pkg_file_list) temp_pkg_file_list = "temp_file_list" @@ -235,16 +341,20 @@ class FileInstaller show_file_list_command = nil extrach_file_list_command = nil + log = "" case ext when ".zip" then show_file_list_command = "zip -sf #{package_file_path}" - extract_file_list_command = "unzip \"#{package_file_path}\" -d \"#{path}\"" + extract_file_list_command = "unzip -o \"#{package_file_path}\" -d \"#{path}\"" when ".tar" then - show_file_list_command = "tar -sf #{package_file_path}" - extract_file_list_command = "tar xf \"#{package_file_path}\" -C \"#{path}\"" + # path should be unix path if it is used in tar command + _package_file_path = Utils.get_unix_path(package_file_path) + _path = Utils.get_unix_path(path) + show_file_list_command = "tar -tf #{_package_file_path}" + extract_file_list_command = "tar xf \"#{_package_file_path}\" -C \"#{_path}\"" else - @@log.error "\"#{filename}\" is not supported." + logger.error "\"#{filename}\" is not supported." return nil end @@ -259,15 +369,44 @@ class FileInstaller end end end - File.delete(temp_pkg_file_list_path) - log = `#{extract_file_list_command}` - @@log.info "Extracted \"#{filename}\" file.. OK" - if log.nil? then log = "" end + + case ext + when ".zip" then + if Utils.is_windows_like_os( Utils::HOST_OS ) then + log = unzip_file(package_file_path, path) + else + #result = Utils.execute_shell_return(extract_file_list_command) + #if result.nil? then log = nil + #else log = result.join("") end + log = `#{extract_file_list_command}` + end + when ".tar" then + #result = Utils.execute_shell_return(extract_file_list_command) + #if result.nil? then log = nil + #else log = result.join("") end + log = `#{extract_file_list_command}` + end + + if log == "" then log = nil end + if log.nil? then + logger.error "Failed to extract \"#{filename}\" file" + logger.info " [file: #{package_file_path}]" + logger.info " [from: #{path}]" + logger.info " [to: #{target_path}]" + logger.info " [cmd: #{extract_file_list_command}]" + return nil + end + + logger.info "Extracted \"#{filename}\" file.. OK" + log = log + "[file: #{package_file_path}]\n" + log = log + "[from: #{path}]\n" + log = log + "[to: #{target_path}]\n" + log = log + "[cmd: #{extract_file_list_command}]\n" return log end - def FileInstaller.extract_specified_file(package_file_path, target_file, path) + def FileInstaller.extract_a_file(package_file_path, target_file, path, logger) dirname = File.dirname(package_file_path) filename = File.basename(package_file_path) ext = File.extname(filename) @@ -280,11 +419,13 @@ class FileInstaller extract_file_command = "unzip -x #{package_file_path} #{target_file}" end when ".tar" then + # path should be unix path if it is used in tar command + _package_file_path = Utils.get_unix_path(package_file_path) + _path = Utils.get_unix_path(path) if not path.nil? then - path = File.join(path, package_file_path) - extract_file_command = "tar xvf #{package_file_path} #{target_file}" + extract_file_command = "tar xf #{_package_file_path} -C #{_path} #{target_file}" else - extract_file_command = "tar xvf #{package_file_path} #{target_file}" + extract_file_command = "tar xf #{_package_file_path} #{target_file}" end end @@ -297,12 +438,46 @@ class FileInstaller end if File.exist? target_file_path then - @@log.info "Extracted \"#{target_file}\" file.." + logger.info "Extracted \"#{target_file}\" file.." return true else - @@log.info "Failed to extracted \"#{target_file}\" file.." + logger.warn "Failed to extracted \"#{target_file}\" file.." + logger.info " [file: #{package_file_path}]" + logger.info " [path: #{path}]" + logger.info " [cmd: #{extract_file_command}]" return false end end -end + def FileInstaller.unzip_file(zipfile, dest) + log = "" + Zip::ZipFile.open(zipfile) do |zip_file| + zip_file.each do |f| + f_path = File.join(dest, f.name) + FileUtils.mkdir_p(File.dirname(f_path)) + if File.exist?(f_path) then + log = log + "[Warn] Exist file : #{f_path}\n" unless f_path.end_with? "/" + else + zip_file.extract(f, f_path) + if not f_path.end_with? "/" then + log = log + "[info] Extracted file : #{f_path}\n" + end + end + end + end + return log + end + + def FileInstaller.unzip_a_file(zipfile, file, dest) + Zip::ZipFile.open(zipfile) do |zip_file| + zip_file.each do |f| + if f.name.strip == file then + f_path = File.join(dest, f.name) + FileUtils.mkdir_p(File.dirname(f_path)) + zip_file.extract(f, f_path) unless File.exist?(f_path) + break + end + end + end + end +end diff --git a/src/pkg_server/packageServer.rb b/src/pkg_server/packageServer.rb index 86abc39..48dd649 100644 --- a/src/pkg_server/packageServer.rb +++ b/src/pkg_server/packageServer.rb @@ -29,266 +29,250 @@ Contributors: require 'fileutils' $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/build_server" +require "BuildComm" require "packageServerLog" require "packageServerConfig" require "distribution" +require "SocketRegisterListener" require "client" require "utils" require "mail" +require "DistSync" class PackageServer - attr_accessor :id, :location, :log, :integrity + attr_accessor :id, :location, :log, :integrity + attr_accessor :finish, :port + attr_accessor :incoming_path + attr_accessor :distribution_list + attr_accessor :sync_interval, :passwd - # constant - SUPPORTED_OS = ["linux", "windows", "darwin"] + # constant + SERVER_ROOT = "#{PackageServerConfig::CONFIG_ROOT}/pkg_server" + DIBS_LOCK_FILE_PATH = "#{SERVER_ROOT}/.server_loc" # initialize def initialize (id) - @id = id @location = "" @distribution_list = [] # distribution name -> server_url hash @dist_to_server_url = {} - @integrity = "NO" - - if not File.exist?( PackageServerConfig::SERVER_ROOT ) - FileUtils.mkdir_p( PackageServerConfig::SERVER_ROOT ) + @integrity = "YES" + @auto_sync_flag = "NO" + @finish = false + @port = 3333 + @test_time=0 #test time in mili-seconds + @lock_file= nil + @sync_interval = 3600 + @passwd = "" + + update_config_information(id) + + if not File.exist?( SERVER_ROOT ) + FileUtils.mkdir_p( SERVER_ROOT ) end - @log = PackageServerLog.new( "#{PackageServerConfig::SERVER_ROOT}/.#{@id}.log" ) + @log = PackageServerLog.new( @log_file_path ) server_information_initialize() end # create - def create (id, dist_name, server_url, loc = nil ) - @id = id + def create( id, dist_name, server_url, loc = nil ) + update_config_information(id) + if loc.nil? or loc.empty? then - @location = Dir.pwd + "/" + id + @location = Dir.pwd + "/" + @id else - if loc.end_with? "/" then - @location = loc + id - else - @location = loc + "/" + id + if Utils.is_absolute_path(loc) then + @location = File.join(loc, @id) + else + @location = File.expand_path(File.join(Dir.pwd, loc, @id)) end end + # error check : check for already exist in server @id + if File.exist? @config_dir + raise RuntimeError, "Server create fail. server id [#{@id}] is already exist" + end + + # name check + if dist_name.strip.eql? "distribution.info" then + raise RuntimeError, "id \"distribution.info\" is not available" + end + # create locking file - File.open("#{PackageServerConfig::LOCK_FILE}", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - # error check : check for already exist in server id - if File.exist? "#{PackageServerConfig::SERVER_ROOT}/#{id}" - raise RuntimeError, "Server create fail. server id [#{id}] is already exist" - end - - # error check : check for already exist in server directory - if File.exist? "#{@location}/#{dist_name}" - raise RuntimeError, "Server create fail. directory is already exist [#{@location}/#{dist_name}]" - end - - # create server config directory - FileUtils.mkdir_p "#{PackageServerConfig::SERVER_ROOT}/#{id}" - FileUtils.mkdir_p "#{PackageServerConfig::SERVER_ROOT}/#{id}/incoming" + lock_file = Utils.file_lock(DIBS_LOCK_FILE_PATH) + + # create server config directory + FileUtils.mkdir_p @config_dir + FileUtils.mkdir_p @incoming_path - if (not server_url.empty?) and (not Utils.is_url_remote(server_url)) + if (not server_url.empty?) and \ + (not Utils.is_url_remote(server_url)) and \ + (not Utils.is_absolute_path(server_url)) then # if server_url is local server address then generate absoulte path - if not Utils.is_absolute_path( server_url ) then - if server_url.end_with?("/") then - server_url = Utils::WORKING_DIR + server_url - else - server_url = Utils::WORKING_DIR + "/" + server_url - end - end - end + server_url = File.join(Utils::WORKING_DIR, server_url) + end - # create server configure file - File.open( "#{PackageServerConfig::SERVER_ROOT}/#{id}/config", "w" ) do |f| - f.puts "location : #{@location}" - f.puts "integrity check : NO" - f.puts "server_url : #{dist_name} -> #{server_url}" - end - - # create location's directory - FileUtils.mkdir_p "#{@location}" - - create_distribution_struct( dist_name, server_url ) - } + # create server configure file + File.open( @config_file_path, "w" ) do |f| + f.puts "location : #{@location}" + f.puts "integrity check : #{@integrity}" + f.puts "auto sync : #{@auto_sync_flag}" + f.puts "sync interval : #{@sync_interval}" + f.puts "server_url : #{dist_name} -> #{server_url}" + end + + # create location's directory + FileUtils.mkdir_p "#{@location}" + + create_distribution_struct( dist_name, server_url ) + Utils.file_unlock(lock_file) @log.output( "package server [#{@id}] created successfully", Log::LV_USER ) end - def register( source_pkg_file_path_list, binary_pkg_file_path_list, dist_name, snapshot, test ) + def register( file_path_list, dist_name, snapshot, test_flag, internal_flag = false ) @log.info "package register in server" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) # distribution lock - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) + @lock_file = Utils.file_lock(distribution.lock_file_path) - source_pkg_file_name_list = [] - updated_os_list = [] - append_pkg_list = [] - used_source_pkg_list = [] - package_list = [] - - # error check - source_pkg_file_path_list.each do |l| - # error check for file exist - if not File.exist? l - raise RuntimeError, "source package file does not exist [#{l}]" - end + updated_os_list = [] + registed_package_list = [] + binary_pkg_file_path_list = [] + link_pkg_file_path_list = [] + archive_pkg_file_path_list = [] + snapshot_name = "" + + file_path_list.each do |f| + # error check for file exist + if not File.exist? f + raise RuntimeError, "package file does not exist [#{f}]" + end - source_pkg_file_name_list.push File.basename( l ) - end - binary_pkg_file_path_list.each do |l| - # error check for file exist - if not File.exist? l - raise RuntimeError, "binary package file does not exist [#{l}]" - end - end + pkg = distribution.get_package_from_file(f) - # register binary package - binary_pkg_file_path_list.each do |l| - # get package class using bianry file - pkg = distribution.get_package_from_file(l) + # binary package + if not pkg.nil? then - if pkg.nil? or pkg.package_name.empty? then - raise "[#{l}]'s pkginfo.manifest file is incomplete." - end - package_list.push pkg - - if test then - if not pkg.source.empty? then - if not source_pkg_file_name_list.include? "#{pkg.source}_#{pkg.version}.tar.gz" - raise "binary package and source package must be upload same time" - end - - pkg.src_path = "/temp/#{pkg.source}_#{pkg.version}.tar.gz" - end - pkg = distribution.register_for_test(l ,pkg ) + # find link package + pkg_os = Utils.get_os_from_package_file(f) + link_pkg = distribution.get_link_package(pkg, pkg_os) + if link_pkg.nil? then + binary_pkg_file_path_list.push f else - if pkg.package_name.empty? or pkg.version.empty? or pkg.os.empty? or pkg.maintainer.empty? then - raise "[#{l}]'s pkginfo.manifest file is incomplete." - # binary only package - elsif pkg.attribute.include? "binary" then - @log.info "binary package [#{l}] is binary only package" - pkg.src_path = "" - elsif pkg.source.empty? then - raise "[#{l}]'s pkginfo.manifest file is incomplete." - # binary package - else - if not source_pkg_file_name_list.include? "#{pkg.source}_#{pkg.version}.tar.gz" - raise "binary package [#{pkg.package_name}]'s source package must be upload same time" - end - - @log.info "binary package [#{l}]'s source package is #{pkg.source}" - used_source_pkg_list.push "#{pkg.source}_#{pkg.version}.tar.gz" - pkg.src_path = "/source/#{pkg.source}_#{pkg.version}.tar.gz" - end - - pkg = distribution.register(l ,pkg ) - updated_os_list.push pkg.os + link_pkg_file_path_list.push [link_pkg.path, File.basename(f)] + pkg.checksum = link_pkg.checksum + pkg.size = link_pkg.size end - - append_pkg_list.push pkg - end - # check install dependency integrity - if not test then distribution.check_integrity end - - source_pkg_file_path_list.each do |source_path| - source_name = File.basename(source_path) - if File.exist? "#{@location}/#{dist_name}/source/#{source_name}" then - @log.warn "source package already exist then does not register" - next - end - - if test then - @log.info "source package [#{source_name}] register in temp/]" - FileUtils.cp( source_path, "#{@location}/#{dist_name}/temp/" ) + # update os information + if pkg.os_list.include? pkg_os then + pkg.os = pkg_os + pkg.os_list = [pkg_os] else - @log.info "source package [#{source_name}] register in source/]" - FileUtils.cp( source_path, "#{@location}/#{dist_name}/source/" ) + raise RuntimeError, "package file name is incorrect [#{f}]" end - end - # register archive pakcage list. - distribution.write_archive_pkg_list( source_pkg_file_name_list - used_source_pkg_list ) + updated_pkg = register_package(distribution, pkg, f, test_flag, internal_flag) + + updated_os_list.push updated_pkg.os + registed_package_list.push updated_pkg + # archive package + else + if test_flag then + @log.error("archive package does not using test mode", Log::LV_USER) + return + end - binary_pkg_file_path_list.each do |l| - if test then - FileUtils.cp( l, "#{@location}/#{dist_name}/temp/" ) - else - FileUtils.cp( l, "#{@location}/#{dist_name}/binary/" ) - end + file_name = File.basename(f) + distribution.register_archive_pkg(file_name) + archive_pkg_file_path_list.push f + end + end + + # check install dependency integrity + if not test_flag and @integrity.eql? "YES" then + registed_package_list.each do |pkg| + distribution.check_package_integrity(pkg) end + end - # write package list for updated os - updated_os_list.uniq! - updated_os_list.each do |os| - distribution.write_pkg_list( os ) + # move file to package server + binary_pkg_file_path_list.each do |l| + if test_flag then + FileUtils.copy_file( l, "#{distribution.location}/temp/#{File.basename(l)}" ) + else + FileUtils.copy_file( l, "#{distribution.location}/binary/#{File.basename(l)}" ) + end + end + + # link to package server + link_pkg_file_path_list.each do |l| + if test_flag then + src_file = File.join(distribution.location, l[0]) + dest_file = File.join(distribution.location, "temp", l[1]) + FileUtils.ln( src_file, dest_file, :force => true ) + else + src_file = File.join(distribution.location, l[0]) + dest_file = File.join(distribution.location, "binary", l[1]) + FileUtils.ln( src_file, dest_file, :force => true ) end + end - # if snapshot mode is true then generate snapshot - if snapshot or test then - @log.info "generaging snapshot" - distribution.generate_snapshot("", "", "") - end + archive_pkg_file_path_list.each do |l| + FileUtils.mv( l, "#{distribution.location}/source/" ) + end - # send email - if not test then - msg_list = [] + # write package list for updated os + updated_os_list.uniq! + updated_os_list.each do |os| + distribution.write_pkg_list(os) + end - package_list.map{ |p| - msg_list.push("%-30s: %08s" % [ p.package_name.strip, p.version.strip ] ) - } - # email just remote package server - # Mail.send_package_registe_mail( msg_list, @id ) - end - } + # register archive pakcage list. + distribution.write_archive_pkg_list() + + # send email + if test_flag then + msg_list = [] + + registed_package_list.each { |p| + msg_list.push("%-30s: %08s" % [ p.package_name.strip, p.version.strip ] ) + } + # email just remote package server + # Mail.send_package_registe_mail( msg_list, @id ) + end + + # if snapshot mode is true then generate snapshot + if snapshot or test_flag then + @log.info "generaging snapshot" + snapshot_name = distribution.generate_snapshot("", "", false) + end + + Utils.file_unlock(@lock_file) @log.output( "package registed successfully", Log::LV_USER) + + return snapshot_name end - def generate_snapshot( snpashot_name, dist_name, base_snapshot, binary_pkg_file_path_list) + def generate_snapshot( snpashot_name, dist_name, base_snapshot ) @log.info "generating snapshot" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - append_pkg_list = [] - binary_pkg_file_path_list.each do |l| - if not File.exist? l then raise RuntimeError,"Can't find binary package file [#{l}]" end - pkg = distribution.get_package_from_file(l) - if l.start_with? "/" - pkg.path = "#{l}" - else - pkg.path = "#{Dir.pwd}/#{l}" - end - append_pkg_list.push pkg - end - - distribution.generate_snapshot( snpashot_name, base_snapshot, append_pkg_list) - } + @lock_file = Utils.file_lock(distribution.lock_file_path) + + snapshot_name = distribution.generate_snapshot( snpashot_name, base_snapshot, true) + + Utils.file_unlock(@lock_file) + + return snapshot_name end def sync( dist_name, mode ) @log.info "sync from server" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) if distribution.server_url.empty? then @@ -296,67 +280,69 @@ class PackageServer return end - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - distribution.sync( mode, "linux" ) - distribution.sync( mode, "windows" ) - distribution.sync( mode, "darwin" ) - distribution.sync_archive_pkg - } + ret = distribution.sync(mode) + if ret then + distribution.generate_snapshot("", "", false) + end - @log.output( "package server [#{@id}]'s distribution [#{dist_name}] has the synchronization.", Log::LV_USER ) + @log.output( "package server [#{@id}]'s distribution [#{dist_name}] has been synchronized.", Log::LV_USER ) end def add_distribution( dist_name, server_url, clone ) - File.open("#{PackageServerConfig::LOCK_FILE}", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - # error check : check for already exist in server directory - if @dist_to_server_url.keys.include? dist_name.strip then - raise RuntimeError, "distribution already exist : #{dist_name}" - end - if File.exist? "#{@location}/#{dist_name}" - raise RuntimeError, "distribution directory already exist [#{@location}/#{dist_name}]" - end - - if (not server_url.empty?) and (not Utils.is_url_remote(server_url)) - # if server_url is local server address then generate absoulte path - if not Utils.is_absolute_path( server_url ) then - if server_url.end_with?("/") then - server_url = Utils::WORKING_DIR + server_url - else - server_url = Utils::WORKING_DIR + "/" + server_url - end - end - end - - File.open( "#{PackageServerConfig::SERVER_ROOT}/#{@id}/config", "a" ) do |f| - if clone then - @log.info "add distribution using [#{server_url}] in clone mode" - f.puts "server_url : #{dist_name} -> " + lock_file = Utils.file_lock(@server_lock_file_path) + + # error check : check for already exist in server directory + if @dist_to_server_url.keys.include? dist_name.strip then + Utils.file_unlock(@lock_file) + raise RuntimeError, "distribution already exist : #{dist_name}" + end + + # name check + if dist_name.strip.eql? "distribution.info" then + Utils.file_unlock(@lock_file) + raise RuntimeError, "id \"distribution.info\" is not available" + end + + # modify server url + if (not server_url.empty?) and (not Utils.is_url_remote(server_url)) + # if server_url is local server address then generate absoulte path + if not Utils.is_absolute_path( server_url ) then + if server_url.end_with?("/") then + server_url = Utils::WORKING_DIR + server_url else - @log.info "add distribution using [#{server_url}]" - f.puts "server_url : #{dist_name} -> #{server_url}" + server_url = Utils::WORKING_DIR + "/" + server_url end end - - create_distribution_struct( dist_name, server_url ) - } + end + + add_dist_for_config_file(dist_name, server_url, clone) + create_distribution_struct( dist_name, server_url ) + + Utils.file_unlock(lock_file) @log.output( "distribution [#{dist_name}] added successfully", Log::LV_USER ) end - def remove_server( id ) - @log.info( "Package server [#{id}] will be removed and all server information delete", Log::LV_USER) + def add_os(dist_name, os) + dist = get_distribution(dist_name) + + # distribution lock + @lock_file = Utils.file_lock(dist.lock_file_path) + + dist.add_os(os) + + @log.info "generaging snapshot" + dist.generate_snapshot("", "", false) - if File.exist? "#{PackageServerConfig::SERVER_ROOT}/#{id}/config" then - File.open "#{PackageServerConfig::SERVER_ROOT}/#{id}/config" do |f| + Utils.file_unlock(@lock_file) + @log.output( "package server add os [#{os}] successfully", Log::LV_USER ) + end + + def remove_server() + @log.info( "Package server [#{@id}] will be removed and all server information delete", Log::LV_USER) + + lock_file = Utils.file_lock(DIBS_LOCK_FILE_PATH) + if File.exist? @config_file_path then + File.open @config_file_path do |f| f.each_line do |l| if l.start_with?( "location : ") then location= l.split(" : ")[1] @@ -366,78 +352,211 @@ class PackageServer end end else - @log.error( "Can't find server information : #{id}", Log::LV_USER) + @log.error( "Can't find server information : #{@id}", Log::LV_USER) end - FileUtils.rm_rf "#{PackageServerConfig::SERVER_ROOT}/.#{id}.log" - FileUtils.rm_rf "#{PackageServerConfig::SERVER_ROOT}/#{id}" + FileUtils.rm_rf @config_dir + FileUtils.rm_rf @log_file_path + + Utils.file_unlock(lock_file) + @log.output( "package server [#{@id}] removed successfully", Log::LV_USER ) + end + + def remove_dist( dist_name ) + @log.info "remove distribution in server" + distribution = get_distribution( dist_name ) + + lock_file = Utils.file_lock(@server_lock_file_path) + + # modify config file + config_file = File.readlines(@config_file_path) + File.open(@config_file_path, 'w') do |f| + config_file.each { |line| + f.puts(line) if not line =~ /server_url : #{dist_name} ->/ + } + end + + # modify info file + config_file = File.readlines("#{@location}/distribution.info") + File.open("#{@location}/distribution.info", 'w') do |f| + remove_flag = false + config_file.each { |line| + if line.start_with? "name :" then + if line.split(':')[1].strip.eql? dist_name then + remove_flag = true + else + remove_flag = false + end + + end + + # rewrite information for not remove distribution + if not remove_flag then + f.puts line + end + } + end + + # remove distribution directory + FileUtils.rm_rf distribution.location - @log.output( "package server [#{id}] removed successfully", Log::LV_USER ) + # remove distribution struct + @distribution_list.delete distribution + + Utils.file_unlock(lock_file) end - def remove_pkg( id, dist_name, pkg_name_list, os ) + def remove_pkg( dist_name, pkg_name_list, os ) @log.info "package remove in server" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) - # distribution lock - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - distribution.remove_pkg(pkg_name_list, os) - } + lock_file = Utils.file_lock(@server_lock_file_path) + + distribution.remove_pkg(pkg_name_list, os) + + # generate snapshot + @log.info "generaging snapshot" + distribution.generate_snapshot("", "", false) + + Utils.file_unlock(lock_file) @log.output( "package removed successfully", Log::LV_USER ) end + + def remove_snapshot( dist_name, snapshot_list ) + @log.info "remove snapshot in server" + distribution = get_distribution( dist_name ) - def find_source_package_path( dist_name, pkg_file_name_list ) - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end + lock_file = Utils.file_lock(@server_lock_file_path) + + distribution.remove_snapshot(snapshot_list) + + Utils.file_unlock(lock_file) + end + + def clean( dist_name, snapshot_list ) + @log.info "pakcage server clean" distribution = get_distribution( dist_name ) - pkg_file_name_list.each do |pkg| - pkg_path = "#{@location}/#{dist_name}/source/#{pkg}" - if File.exist? pkg_path then - @log.output( "#{pkg}", Log::LV_USER) + lock_file = Utils.file_lock(@server_lock_file_path) + + distribution.clean( snapshot_list ) + + # remove incoming dir + FileUtils.rm_rf incoming_path + FileUtils.mkdir incoming_path + + Utils.file_unlock(lock_file) + end + + # start server daemon + def start( port, passwd ) + @log.info "Package server Start..." + # set port number. default port is 3333 + @port = port + + # set job request listener + @log.info "Setting listener..." + listener = SocketRegisterListener.new(self) + listener.start + + # set auto sync + if @auto_sync_flag.eql? "YES" then + @log.info "Setting auto sync..." + autosync = DistSync.new(self) + autosync.start + end + + # set password + @passwd = passwd + + # main loop + @log.info "Entering main loop..." + if @test_time > 0 then start_time = Time.now end + while( not @finish ) + # sleep + if @test_time > 0 then + curr_time = Time.now + if (curr_time - start_time).to_i > @test_time then + puts "Test time is elapsed!" + break + end else - @log.error( "Can't find [#{pkg}] in source package", Log::LV_USER) + sleep 1 end - end - end + end + end + + # stop server daemon + def stop( port, passwd ) + # set port number. default port is 3333 + @port = port + @finish = false + + client = BuildCommClient.create("127.0.0.1", @port, @log) + if client.nil? then + raise RuntimeError, "Server does not listen in #{@port} port" + end + + client.send("STOP|#{passwd}") + + ret = client.receive_data + if ret[0].strip.eql? "SUCC" then + @log.output( "Package server is stopped", Log::LV_USER) + else + @log.output( "Package server return error message : #{ret}", Log::LV_USER) + end + client.terminate + + end def self.list_id - @@log = PackageServerLog.new( "#{PackageServerConfig::SERVER_ROOT}/.log" ) + @@log = PackageServerLog.new("#{SERVER_ROOT}/.log") - d = Dir.new( PackageServerConfig::SERVER_ROOT ) + d = Dir.new( SERVER_ROOT ) s = d.select {|f| not f.start_with?(".") } s.sort! + server_list = [] @@log.output( "=== server ID list ===", Log::LV_USER) s.each do |id| + if File.extname(id).eql?(".log") then next end + + server_list.push id @@log.output( id, Log::LV_USER) end + @@log.close + FileUtils.rm_rf("#{SERVER_ROOT}/.log") + + return server_list end def self.list_dist( id ) - @@log = PackageServerLog.new( "#{PackageServerConfig::SERVER_ROOT}/.log" ) + @@log = PackageServerLog.new( "#{SERVER_ROOT}/.log" ) @@log.output( "=== ID [#{id}]'s distribution list ===", Log::LV_USER) + dist_list = [] + # read package id information - if File.exist? "#{PackageServerConfig::SERVER_ROOT}/#{id}/config" then - File.open "#{PackageServerConfig::SERVER_ROOT}/#{id}/config" do |f| - f.each_line do |l| - if l.start_with?( "server_url : ") and l.include?( "->" ) then - @@log.output( l.split(" : ")[1].split("->")[0], Log::LV_USER) - end - end - end - else + config_file_path = "#{SERVER_ROOT}/#{id}/config" + if not File.exist? config_file_path raise RuntimeError, "[#{id}] is not server ID" end + + File.open config_file_path do |f| + f.each_line do |l| + if l.start_with?( "server_url : ") and l.include?( "->" ) then + dist_name = l.split(" : ")[1].split("->")[0] + + dist_list.push dist_name + @@log.output( dist_name, Log::LV_USER) + end + end + end + @@log.close + FileUtils.rm_rf("#{SERVER_ROOT}/.log") + + return dist_list end def get_default_dist_name() @@ -447,13 +566,28 @@ class PackageServer return @distribution_list[0].name end + def reload_dist_package() + # create locking file + lock_file = Utils.file_lock(@server_lock_file_path) + @distribution_list.each do |dist| + dist.initialize_pkg_list + end + Utils.file_unlock(lock_file) + end + + def release_lock_file + if not @lock_file.nil? then + Utils.file_unlock(@lock_file) + end + end + # PRIVATE METHODS/VARIABLES private def server_information_initialize # if id is nil or empty then find default id if @id.nil? or @id.empty? - d = Dir.new( PackageServerConfig::SERVER_ROOT ) + d = Dir.new( SERVER_ROOT ) s = d.select {|f| not f.start_with?(".") } if s.length.eql? 1 then @log.info "using default server ID [#{s[0]}]" @@ -464,29 +598,40 @@ class PackageServer end # read package id information - if File.exist? PackageServerConfig::SERVER_ROOT and File.exist? "#{PackageServerConfig::SERVER_ROOT}/#{@id}/config" then - File.open "#{PackageServerConfig::SERVER_ROOT}/#{@id}/config" do |f| + if File.exist? @config_file_path + File.open @config_file_path do |f| f.each_line do |l| - if l.start_with?( "location : ") then - @location = l.split(" : ")[1].strip - elsif l.start_with?( "integrity check : ") then - @integrity = l.split(" : ")[1].strip - elsif l.start_with?( "server_url : " ) then - info = l.split(" : ")[1].split("->") + if l.start_with?( "location :") then + @location = l.split(" :")[1].strip + elsif l.start_with?( "integrity check :") then + @integrity = l.split(" :")[1].strip.upcase + elsif l.start_with?( "auto sync :" ) then + @auto_sync_flag = l.split(" :")[1].strip.upcase + elsif l.start_with?( "sync interval :" ) then + @sync_interval = l.split(" :")[1].strip.to_i + elsif l.start_with?( "server_url :" ) then + info = l.split(" :")[1].split("->") @dist_to_server_url[info[0].strip] = info[1].strip else @log.error "server config file has invalid information [#{l}]" end end end - end - @dist_to_server_url.each do |dist_name, server_url| - @distribution_list.push Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, self ) + @dist_to_server_url.each do |dist_name, server_url| + @distribution_list.push Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, self ) + end end end def get_distribution( dist_name ) + if dist_name.nil? or dist_name.empty? then + dist_name = get_default_dist_name() + end + if dist_name.empty? then + raise RuntimeError,"Can't find distribution information" + end + @distribution_list.each do |dist| if dist.name.eql? dist_name.strip return dist @@ -497,12 +642,19 @@ class PackageServer end def create_distribution_struct( dist_name, server_url ) + if File.exist? "#{@location}/#{dist_name}" + raise RuntimeError, "distribution directory already exist [#{@location}/#{dist_name}]" + end + FileUtils.mkdir "#{@location}/#{dist_name}" FileUtils.mkdir "#{@location}/#{dist_name}/binary" FileUtils.mkdir "#{@location}/#{dist_name}/source" FileUtils.mkdir "#{@location}/#{dist_name}/temp" FileUtils.mkdir "#{@location}/#{dist_name}/snapshots" - + File.open("#{@location}/#{dist_name}/#{Distribution::SNAPSHOT_INFO_FILE}", "w") {} + File.open("#{@location}/#{dist_name}/#{Distribution::OS_INFO_FILE}", "w") {} + File.open("#{@location}/#{dist_name}/#{Distribution::ARCHIVE_PKG_FILE}", "w") {} + # generate distribution distribution = Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, self ) @@ -517,20 +669,64 @@ class PackageServer else @log.info "[#{dist_name}] distribution creation. using local server [#{server_url}]" end - - distribution.sync( false, "linux" ) - distribution.sync( false, "windows" ) - distribution.sync( false, "darwin" ) - distribution.sync_archive_pkg + + distribution.sync(false) + distribution.generate_snapshot("", "", false) else @log.info "generate package server do not using remote package server" # write_pkg_list for empty file - distribution.write_pkg_list( "linux" ) - distribution.write_pkg_list( "windows" ) - distribution.write_pkg_list( "darwin" ) - distribution.write_archive_pkg_list( "" ) + distribution.write_pkg_list(nil) + distribution.write_archive_pkg_list() + end + + # add dist information to distribution.info file + File.open("#{@location}/distribution.info", "a") do |f| + f.puts "name : #{dist_name}" + f.puts "time : #{Time.now.strftime("%Y%m%d%H%M%S")}" + f.puts end end -end + def register_package(distribution, pkg, file_path, test_flag, internal_flag) + # get package class using bianry file + if pkg.nil? or pkg.package_name.empty? then + raise "[#{file_path}]'s pkginfo.manifest file is incomplete." + end + + if not test_flag then + # error check + if pkg.package_name.empty? or pkg.version.empty? \ + or pkg.os.empty? or pkg.maintainer.empty? then + raise "[#{file_path}]'s pkginfo.manifest file is incomplete." + end + + updated_pkg = distribution.register(file_path, pkg, internal_flag ) + else + updated_pkg = distribution.register_for_test(file_path, pkg ) + end + + return updated_pkg + end + + def add_dist_for_config_file(dist_name, server_url, clone) + File.open( @config_file_path, "a" ) do |f| + if clone then + @log.info "add distribution using [#{server_url}] in clone mode" + f.puts "server_url : #{dist_name} -> " + else + @log.info "add distribution using [#{server_url}]" + f.puts "server_url : #{dist_name} -> #{server_url}" + end + end + end + + def update_config_information(id) + @id = id + @config_dir = "#{SERVER_ROOT}/#{@id}" + @log_file_path = "#{SERVER_ROOT}/#{@id}.log" + @config_file_path = "#{@config_dir}/config" + @incoming_path = "#{@config_dir}/incoming" + @server_lock_file_path = "#{@config_dir}/.server_lock" + end +end diff --git a/src/pkg_server/packageServerConfig.rb b/src/pkg_server/packageServerConfig.rb index 7e19000..b2b447a 100644 --- a/src/pkg_server/packageServerConfig.rb +++ b/src/pkg_server/packageServerConfig.rb @@ -31,6 +31,4 @@ require "utils" class PackageServerConfig CONFIG_ROOT = "#{Utils::HOME}/.build_tools" - SERVER_ROOT = "#{PackageServerConfig::CONFIG_ROOT}/pkg_server" - LOCK_FILE = "#{PackageServerConfig::SERVER_ROOT}/.server_loc" end diff --git a/src/pkg_server/serverOptParser.rb b/src/pkg_server/serverOptParser.rb index da06a21..fed7ace 100644 --- a/src/pkg_server/serverOptParser.rb +++ b/src/pkg_server/serverOptParser.rb @@ -31,19 +31,21 @@ $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "utils" def set_default( options ) - if options[:id].nil? then options[:id] = "" end - if options[:dist].nil? then options[:dist] = "" end - if options[:url].nil? then options[:url] = "" end - if options[:os].nil? then options[:os] = "all" end - if options[:bpkgs].nil? then options[:bpkgs] = [] end - if options[:apkgs].nil? then options[:apkgs] = [] end - if options[:spkgs].nil? then options[:spkgs] = [] end - if options[:snap].nil? then options[:snap] = "" end - if options[:bsnap].nil? then options[:bsnap] = "" end - if options[:gensnap].nil? then options[:gensnap] = false end - if options[:force].nil? then options[:force] = false end - if options[:test].nil? then options[:test] = false end - if options[:clone].nil? then options[:clone] = false end + options[:id] = "" + options[:dist] = "" + options[:url] = "" + options[:os] = "all" + options[:pkgs] = [] + options[:snaps] = [] + options[:bsnap] = "" + options[:port] = "3333" + options[:gensnap] = true + options[:force] = false + options[:test] = false + options[:clone] = false + options[:origin_pkg_name] = "" + options[:origin_pkg_os] = "" + options[:passwd] = "" end def option_error_check( options ) @@ -51,57 +53,99 @@ def option_error_check( options ) case options[:cmd] when "create" if options[:id].empty? or options[:dist].empty? then - raise ArgumentError, "Usage: pkg-svr create -i -d [-u ] [-l ] " - end - when "remove-pkg" - if options[:bpkgs].empty? then - raise ArgumentError, "pkg-svr remove-pkg -i -d -p [-o ]" + "\n" \ - end - when "spkg-path" - if options[:spkgs].empty? then - raise ArgumentError, "Usage: pkg-svr spkg-name -i -d -s " + raise ArgumentError, "Usage: pkg-svr create -n -d [-u ] [-l ] " end when "remove" if options[:id].empty? then - raise ArgumentError, "Usage: pkg-svr remove -i " + raise ArgumentError, "Usage: pkg-svr remove -n " + end + when "remove-pkg" + if options[:pkgs].empty? then + raise ArgumentError, "Usage: pkg-svr remove-pkg -n -d -P [-o ]" + "\n" \ + end + when "remove-snapshot" + if options[:snaps].empty? then + raise ArgumentError, "Usage: pkg-svr remove-snapshot -n -d -s " end when "add-dist" if options[:id].empty? or options[:dist].empty? then - raise ArgumentError, "Usage: pkg-svr add-dist -i -d [-u ] [-c] " + raise ArgumentError, "Usage: pkg-svr add-dist -n -d [-u ] [--clone] " + end + when "add-os" + if options[:os].empty? then + raise ArgumentError, "Usage: pkg-svr add-os -n -d -o ] " + end + when "remove-dist" + if options[:id].empty? or options[:dist].empty? then + raise ArgumentError, "Usage: pkg-svr remove-dist -n -d " end when "register" - if options[:bpkgs].empty? and options[:spkgs].empty? then - raise ArgumentError, "Usage: pkg-svr register -i -d -p -s [-g] [-t] " + if options[:pkgs].empty? then + raise ArgumentError, "Usage: pkg-svr register -n -d -P [--gen] [--test] " end - when "remove" when "gen-snapshot" + if options[:snaps].empty? then + raise ArgumentError, "Usage: pkg-svr gen-snapshot -n -d -s [-b ]" + end + when "start" + if options[:port].empty? then + raise ArgumentError, "Usage: pkg-svr start -n -p [-w ]" + end + when "stop" + if options[:port].empty? then + raise ArgumentError, "Usage: pkg-svr stop -n -p [-w ]" + end when "sync" when "list" + when "clean" else - raise ArgumentError, "input option incorrect : #{options[:cmd]}" + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" end end def option_parse options = {} - banner = "Usage: pkg-svr {create|register|gen-snapshot|sync|add-dist|spkg-path|remove|remove-pkg|list|help} ..." + "\n" \ - + "\t" + "pkg-svr create -i -d [-u ] [-l ] " + "\n" \ - + "\t" + "pkg-svr add-dist -i -d [-u ] [-c] " + "\n" \ - + "\t" + "pkg-svr remove -i " + "\n" \ - + "\t" + "pkg-svr register -i -d -p -s [-g] [-t] " + "\n" \ - + "\t" + "pkg-svr remove-pkg -i -d -p [-o ] " + "\n" \ - + "\t" + "pkg-svr gen-snapshot -i -d [-n ] [-b ] [-p ] " + "\n" \ - + "\t" + "pkg-svr sync -i -d [-f] " + "\n" \ - + "\t" + "pkg-svr spkg-path -i -d -s " + "\n" \ - + "\t" + "pkg-svr list [-i ] " + "\n" - - optparse = OptionParser.new do|opts| + banner = "Package-server administer service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-svr [OPTS] or pkg-svr (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "create Create a package-server." + "\n" \ + + "\t" + "add-dist Add a distribution to package-server." + "\n" \ + + "\t" + "add-os Add supported os." + "\n" \ + + "\t" + "register Register a package in package-server." + "\n" \ + + "\t" + "remove Remove a package-server." + "\n" \ + + "\t" + "remove-dist Remove a distribution to package-server." + "\n" \ + + "\t" + "remove-pkg Remove a package in package-server." + "\n" \ + + "\t" + "remove-snapshot Remove a snapshot in package-server." + "\n" \ + + "\t" + "gen-snapshot Generate a snapshot in package-server." + "\n" \ + + "\t" + "sync Synchronize the package-server from parent package server." + "\n" \ + + "\t" + "start Start the package-server." + "\n" \ + + "\t" + "stop Stop the package-server." + "\n" \ + + "\t" + "clean Delete unneeded package files in package-server." + "\n" \ + + "\t" + "list Show all pack" + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + "pkg-svr create -n -d [-u ] [-l ] " + "\n" \ + + "\t" + "pkg-svr add-dist -n -d [-u ] [--clone] " + "\n" \ + + "\t" + "pkg-svr add-os -n -d -o " + "\n" \ + + "\t" + "pkg-svr register -n -d -P [--gen] [--test] " + "\n" \ + + "\t" + "pkg-svr remove -n " + "\n" \ + + "\t" + "pkg-svr remove-dist -n -d " + "\n" \ + + "\t" + "pkg-svr remove-pkg -n -d -P [-o ] " + "\n" \ + + "\t" + "pkg-svr remove-snapshot -n -d -s " + "\n" \ + + "\t" + "pkg-svr gen-snapshot -n -d -s [-b ] " + "\n" \ + + "\t" + "pkg-svr sync -n -d [--force] " + "\n" \ + + "\t" + "pkg-svr clean -n -d [-s ] " + "\n" \ + + "\t" + "pkg-svr start -n -p [-w ]" + "\n" \ + + "\t" + "pkg-svr stop -n -p [-w ]" + "\n" \ + + "\t" + "pkg-svr list [-n ] " + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| # Set a banner, displayed at the top # of the help screen. opts.banner = banner - opts.on( '-i', '--id ', 'package server id' ) do|name| + opts.on( '-n', '--name ', 'package server name' ) do|name| options[:id] = name end @@ -109,7 +153,7 @@ def option_parse options[:dist] = dist end - opts.on( '-u', '--url ', 'remote server address' ) do|url| + opts.on( '-u', '--url ', 'remote server url: http://127.0.0.1/dibs/unstable' ) do|url| options[:url] = url end @@ -117,80 +161,89 @@ def option_parse options[:os] = os end - opts.on( '-p', '--bpackage ', 'binary package file path list' ) do|bpkgs| - options[:bpkgs] = [] - list = bpkgs.tr(" \t","").split(",") + opts.on( '-P', '--pkgs ', 'package file path list' ) do|pkgs| + if not Utils.multi_argument_test( pkgs, "," ) then + raise ArgumentError, "Package variable parsing error : #{pkgs}" + end + list = pkgs.tr(" \t","").split(",") list.each do |l| - # TODO: is not working - #reg = Regexp.new(l) - #Dir.entries(Utils::WORKING_DIR).select{|x| x =~ reg}.each do |ls| - # options[:bpkgs].push ls - #end if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:bpkgs].push l + options[:pkgs].push l end end - opts.on( '-s', '--spackage ', 'source package file path ' ) do|spkgs| - options[:spkgs] = [] - list = spkgs.tr(" \t","").split(",") - list.each do |l| - if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:spkgs].push l - end + opts.on( '-s', '--snapshot ', 'a snapshot name or snapshot list' ) do|snaplist| + if not Utils.multi_argument_test( snaplist, "," ) then + raise ArgumentError, "Snapshot variable parsing error : #{snaplist}" + end + options[:snaps] = snaplist.split(",") end - - opts.on( '-g', '--generate', 'snapshot is generate' ) do - options[:gensnap] = true - end - opts.on( '-n', '--sname ', 'snapshot name' ) do|snap| - options[:snap] = snap - end - - opts.on( '-b', '--bsnapshot ', 'base snapshot name' ) do|bsnap| + opts.on( '-b', '--base ', 'base snapshot name' ) do|bsnap| options[:bsnap] = bsnap end - opts.on( '-l', '--location ', 'server location' ) do|loc| + opts.on( '-l', '--loc ', 'server location' ) do|loc| options[:loc] = loc end - opts.on( '-f', '--force', 'force update pkg file' ) do - options[:force] = true + opts.on( '-p', '--port ', 'port number' ) do|port| + options[:port] = port end - opts.on( '-t', '--test', 'upload for test' ) do - options[:test] = true + opts.on( '-w', '--passwd ', 'password for package server' ) do|passwd| + options[:passwd] = passwd end - - opts.on( '-c', '--clone', 'clone mode' ) do + + opts.on( '--clone', 'clone mode' ) do options[:clone] = true end + + opts.on( '--force', 'force update pkg file' ) do + options[:force] = true + end - opts.on( '-h', '--help', 'display this information' ) do + opts.on( '--test', 'upload for test' ) do + options[:test] = true + end + + opts.on( '--gen', 'generate snapshot' ) do + options[:gensnap] = true + end + + opts.on( '-h', '--help', 'display help' ) do puts opts exit end - + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end end cmd = ARGV[0] - if cmd.eql? "create" or cmd.eql? "register" or cmd.eql? "sync" \ - or cmd.eql? "gen-snapshot" or cmd.eql? "add-dist" \ - or cmd.eql? "spkg-path" or cmd.eql? "remove" \ - or cmd.eql? "list" or cmd.eql? "remove-pkg" or cmd =~ /(help)|(-h)|(--help)/ then + if cmd.eql? "create" or cmd.eql? "sync" \ + or cmd.eql? "register" \ + or cmd.eql? "gen-snapshot" \ + or cmd.eql? "add-dist" or cmd.eql? "add-os" \ + or cmd.eql? "remove" or cmd.eql? "remove-dist" \ + or cmd.eql? "remove-pkg" or cmd.eql? "remove-snapshot" \ + or cmd.eql? "start" or cmd.eql? "stop" or cmd.eql? "clean" \ + or cmd.eql? "list" \ + or cmd =~ /(-v)|(--version)/ \ + or cmd =~ /(help)|(-h)|(--help)/ then if cmd.eql? "help" then ARGV[0] = "-h" end options[:cmd] = ARGV[0] else - raise ArgumentError, banner + raise ArgumentError, "Usage: pkg-svr [OPTS] or pkg-svr -h" end - optparse.parse! - # default value setting set_default options + + optparse.parse! # option error check option_error_check options diff --git a/test/a/a b/test/a/a deleted file mode 100644 index 7898192..0000000 --- a/test/a/a +++ /dev/null @@ -1 +0,0 @@ -a diff --git a/test/a/package/build.linux b/test/a/package/build.linux deleted file mode 100755 index e056656..0000000 --- a/test/a/package/build.linux +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -e - -clean () -{ - rm -rf $SRCDIR/*.zip - rm -rf $SRCDIR/*.tar.gz -} - -build() -{ - if [ "`cat $ROOTDIR/b`" = "b" ] - then - echo "A: `cat $ROOTDIR/b` == b ... ok" - else - echo "A: `cat $ROOTDIR/b` != b ... fail" - exit 1 - fi - - if [ "`cat $ROOTDIR/c`" = "ca" ] - then - echo "A: `cat $ROOTDIR/c` == ca ... ok" - else - echo "A: `cat $ROOTDIR/c` != ca ... fail" - exit 1 - fi -} - -install() -{ - mkdir -p $SRCDIR/package/a.package.linux/data - cp $SRCDIR/a $SRCDIR/package/a.package.linux/data -} - -$1 -echo "$1 success" diff --git a/test/a/package/pkginfo.manifest b/test/a/package/pkginfo.manifest deleted file mode 100644 index bb9c561..0000000 --- a/test/a/package/pkginfo.manifest +++ /dev/null @@ -1,7 +0,0 @@ -Package: a -Version: 11 -OS: linux -Maintainer: xxx -Build-host-os: linux -Build-dependency: b [linux], c [linux] -Source: a diff --git a/test/b/b b/test/b/b deleted file mode 100644 index 6178079..0000000 --- a/test/b/b +++ /dev/null @@ -1 +0,0 @@ -b diff --git a/test/b/package/build.linux b/test/b/package/build.linux deleted file mode 100755 index 83e5a6c..0000000 --- a/test/b/package/build.linux +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -e - -clean () -{ - rm -rf $SRCDIR/*.zip - rm -rf $SRCDIR/*.tar.gz -} - -build() -{ - if [ "`cat $ROOTDIR/c`" = "ca" ] - then - echo "B: `cat $ROOTDIR/c` == ca ... ok" - else - echo "B: `cat $ROOTDIR/c` != ca ... fail" - exit 1 - fi -} - -install() -{ - mkdir -p $SRCDIR/package/b.package.linux/data - cp $SRCDIR/b $SRCDIR/package/b.package.linux/data -} - -$1 -echo "$1 success" diff --git a/test/b/package/pkginfo.manifest b/test/b/package/pkginfo.manifest deleted file mode 100644 index a164fd8..0000000 --- a/test/b/package/pkginfo.manifest +++ /dev/null @@ -1,7 +0,0 @@ -Package: b -Version: 11 -OS: linux -Maintainer: xxx -Build-host-os: linux -Build-dependency: c [linux] -Source: b diff --git a/test/bin/bin_0.0.0_linux.zip b/test/bin/bin_0.0.0_linux.zip new file mode 100644 index 0000000000000000000000000000000000000000..9880da1e42ac1986bbbe9bf1b83e51dfec0c1056 GIT binary patch literal 620 zcmWIWW@h1H00I5ZRSsYVlwf6$VMs|VNz@Mw;bdUmRleFEgi9;985mh!Ff%ZKi2xMs z>yUv!>vv06zJ%s%PS>MrH8|cae|y0vT78nwfqVD1 zgo;m1d{AvayS#N*yVzI1@FTDK6em4jvrTROt~*A2M_w-N>ug(AVCVJl@p)V3jZcnG z&}Dohl+9kI_&7oSp`h^gW#9UX7F#cJ+JZycwC~m~jQQ1kleQAi(g}5kx}+o)r@C zXdXp33N_dvMghZ*VM(Js%qUWhFp9!3TR0iX#5+3A^iY597& ziFui6sl_E=vreb-27oY{TT=T^a~(1eaQ*(XtEPc>ku|65(X|>J@0P!vu%y3JWwPu4 zdt0uyPECApX3p-NM-9a(wf{SnKZ`!v_|$Ncz_ zyzW8A=cE()Y&pW7+*OLH4*0tmRcCHhpYZ8X));HK4!H;Tt8G&`TX1a0p5&Ea?H4bTLS2B5D;K^>jFfuR*08J>!PS4Cs%h$_I z%*#wmEiM6@6_|R~AB54|lG^Xcb;v-#^}D4jUxM>pFAc3cSH}YTTxX-)xgJK0)zz27 zVy_%MF#G?%XYmVP9+cWS*>uv5*#Z}X|9OS3TD#+p5ue9P)jrPlZ3T8k4G7CL7 zR&?iC=B*<8qIF7hZHv24O6X(xt8Qf-%_WP^>)LLdRT+9|t-5t*T(3h}YIRlr{nTroteN2~b37q4T!(>-E^Mu-`J->JZycwC~m~jQR1kl$YAi(g}5ky0So)r@G zXkJA(3N_#%Mgc>RVM(Js%qUO2t{-Fun*KyCkO?9{^&AWxVS*XfqWu~PTmw?T>0CWckqnVZ3Z_RheK)|JX{vmdSum-+JL4_z5A)&u&k(TGu zd*75!y!}`uK%i^ty*IPd-UXB<1jwaidfHrMf32}J=4qC8UVqERo+F00-ygBOweOsi zxJB{9<1q(?mOIbC$8*DN67P%F5a#0y&K)TsPur4|qqD1T_ULW>awO|nsOR;*Gb&c5 zOf#p>Rb4b^%D-j%7wH`7ym{N`Q@Zkxz!-y&d;2P8o|34D`_C5O&B!Fjj4Qw;fIbHS z0fx7ZAQ~F{tdQVG^DeqksDTeL3K)tEOB(HAMggM&pINw~0b$mb#zvr7n2`cB6-$H! Uc(bwrl`}8{;aeb`4{{Cz0P`WK*#H0l literal 0 HcmV?d00001 diff --git a/test/bin/src.tar.gz b/test/bin/src.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c41dcd82625abf1d4d8b98322237d81743f6ef37 GIT binary patch literal 150 zcmb2|=3qFVe%7CX`RzqVE+#{s)`x3sUGo#HbA{IjAHCaPQ2OqDh=5B0%fgN`|8qBa zDDcP4^{ak)MqlT-S##=t8+jF$%N7||B#yRE2o{-M&7S?NN_TQ`k<9atH5(7_-tGU} xK6Ls&@8|Ms6MQq5{(rc&-cc`f6c;x4EB{WH#7ScFlaC^003{mKPCVG literal 0 HcmV?d00001 diff --git a/test/build-cli-01.testcase b/test/build-cli-01.testcase index 6e47b8c..f3c8c50 100644 --- a/test/build-cli-01.testcase +++ b/test/build-cli-01.testcase @@ -3,14 +3,38 @@ ../build-cli -h #POST-EXEC #EXPECT -Usage: build-cli {build|resolve|query} ... - build-cli build -g -c [-d ] [-p ] [-o ] [-a ] - build-cli resolve -g -c [-d ] [-p ] [-o ] [-a ] - build-cli query [-d ] [-p ] - -g, --git git repository - -c, --commit git commit id/tag - -d, --domain remote build server ip address. default 127.0.0.1 - -p, --port remote build server port. default 2222 - -o, --os target operating system linux/windows/darwin - -a, --async asynchronous job - -h, --help display this information +Requiest service to build-server command-line tool. + +Usage: build-cli [OPTS] or build-cli (-h|-v) + +Subcommands: +build Build and create package. +resolve Request change to resolve-status for build-conflict. +query Query information about build-server. +query-system Query system information about build-server. +query-project Query project information about build-server. +query-job Query job information about build-server. +cancel Cancel a building project. +register Register the package to the build-server. + +Subcommand usage: +build-cli build -N -d [-o ] [-w ] [--async] +build-cli resolve -N -d [-o ] [-w ] [--async] +build-cli query -d +build-cli query-system -d +build-cli query-project -d +build-cli query-job -d +build-cli cancel -j -d [-w ] +build-cli register -P -d -t [-w ] + +Options: +-N, --project project name +-d, --address build server address: 127.0.0.1:2224 +-o, --os target operating system: ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64 +--async asynchronous job +-j, --job job number +-w, --passwd password for managing project +-P, --pkg package file path +-t, --ftp ftp server url: ftp://dibsftp:dibsftp@127.0.0.1 +-h, --help display help +-v, --version display version diff --git a/test/build-cli-02.testcase b/test/build-cli-02.testcase index c49e5d1..4b13c12 100644 --- a/test/build-cli-02.testcase +++ b/test/build-cli-02.testcase @@ -1,16 +1,29 @@ #PRE-EXEC -mkdir buildsvr01 -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/emptyserver/unstable -d pkgserver -i emptyserver -../build-svr start -n testserver3 -p 2223 & -sleep 1 #EXEC -../build-cli query -d 127.0.0.1 -p 2223 +../build-cli query -d 127.0.0.1:2223 #POST-EXEC -../build-svr stop -n testserver3 -sleep 1 -../build-svr remove -n testserver3 -rm -rf buildsvr01 #EXPECT -HOST-OS: -MAX_WORKING_JOBS: -* JOB * +* SYSTEM INFO * +HOST-OS: ubuntu-32 +MAX_WORKING_JOBS: 2 + +* FTP * +FTP_ADDR: +FTP_USERNAME: + +* SUPPORTED OS LIST * +ubuntu-32 +windows-32 + +* FRIEND SERVER LIST (WAIT|WORK/MAX) jobs [transfer count] * + + +* PROJECT(S) * +testa NORMAL +testa1 NORMAL +testb NORMAL +testc NORMAL +testd NORMAL +teste REMOTE + +* JOB(S) * diff --git a/test/build-cli-03.testcase b/test/build-cli-03.testcase index c49e5d1..4641ffb 100644 --- a/test/build-cli-03.testcase +++ b/test/build-cli-03.testcase @@ -1,16 +1,30 @@ #PRE-EXEC -mkdir buildsvr01 -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/emptyserver/unstable -d pkgserver -i emptyserver -../build-svr start -n testserver3 -p 2223 & -sleep 1 #EXEC -../build-cli query -d 127.0.0.1 -p 2223 +../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 #POST-EXEC -../build-svr stop -n testserver3 -sleep 1 -../build-svr remove -n testserver3 -rm -rf buildsvr01 #EXPECT -HOST-OS: -MAX_WORKING_JOBS: -* JOB * +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-cli-03_1.testcase b/test/build-cli-03_1.testcase new file mode 100644 index 0000000..f082adf --- /dev/null +++ b/test/build-cli-03_1.testcase @@ -0,0 +1,28 @@ +#PRE-EXEC +echo "This is the test case for omitting os" +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-cli-04.testcase b/test/build-cli-04.testcase new file mode 100644 index 0000000..e990744 --- /dev/null +++ b/test/build-cli-04.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +#EXEC +../build-cli build -N non_exist_project -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Error: Requested project does not exist! +Info: Check project name using "query" command option ! diff --git a/test/build-cli-05.testcase b/test/build-cli-05.testcase new file mode 100644 index 0000000..039eb37 --- /dev/null +++ b/test/build-cli-05.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-cli build -N testa -d 127.0.0.1:11113 -o ubuntu-32 +#POST-EXEC +#EXPECT +Connection to server failed! diff --git a/test/build-cli-06.testcase b/test/build-cli-06.testcase new file mode 100644 index 0000000..308410f --- /dev/null +++ b/test/build-cli-06.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-cli build -N testa -d 111.11q.111.111:1111 -o ubuntu-32 +#POST-EXEC +#EXPECT +Connection to server failed! diff --git a/test/build-cli-07.testcase b/test/build-cli-07.testcase new file mode 100644 index 0000000..d59ca4c --- /dev/null +++ b/test/build-cli-07.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +echo "testa project is already built and uploaded in previeous testcase" +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Error: Version must be increased : +Error: Job is stopped by ERROR diff --git a/test/build-cli-08.testcase b/test/build-cli-08.testcase new file mode 100644 index 0000000..ff51d73 --- /dev/null +++ b/test/build-cli-08.testcase @@ -0,0 +1,32 @@ +#PRE-EXEC +echo "Assume testa project is already built and uploaded in previeous testcase" +#EXEC +../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... b_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testb" diff --git a/test/build-cli-09.testcase b/test/build-cli-09.testcase new file mode 100644 index 0000000..676cd46 --- /dev/null +++ b/test/build-cli-09.testcase @@ -0,0 +1,19 @@ +#PRE-EXEC +echo "if build-dep package does not exist in server, will show the error" +echo "Assume testa/testb project is already built and uploaded in previeous testcase" +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P b +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Error: Unmet dependency found! +Error: * a(ubuntu-32) for build-dependency +Error: Job is stopped by ERROR diff --git a/test/build-cli-10.testcase b/test/build-cli-10.testcase new file mode 100644 index 0000000..7ca3ce7 --- /dev/null +++ b/test/build-cli-10.testcase @@ -0,0 +1,32 @@ +#PRE-EXEC +echo "This is the test case for omitting os" +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-cli-11.testcase b/test/build-cli-11.testcase new file mode 100644 index 0000000..87e1de1 --- /dev/null +++ b/test/build-cli-11.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +echo "if there doe not exist server to build, error" +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 -o windows-32 +#POST-EXEC +#EXPECT +Info: Added new job "5" for windows-32! +Info: Initializing job... +Error: No servers that are able to build your packages. +Error: Host-OS (windows-32) is not supported in build server. +Error: Job is stopped by ERROR diff --git a/test/build-cli-12.testcase b/test/build-cli-12.testcase new file mode 100644 index 0000000..7c4154b --- /dev/null +++ b/test/build-cli-12.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +echo "wrong os name in build command" +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 -o wrong_os_name +#POST-EXEC +#EXPECT +Error: Unsupported OS name "wrong_os_name" is used! +Error: Check the following supported OS list. +* ubuntu-32 +* windows-32 diff --git a/test/build-cli-12_1.testcase b/test/build-cli-12_1.testcase new file mode 100644 index 0000000..efaa7cf --- /dev/null +++ b/test/build-cli-12_1.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +echo "wrong os name in resolve command" +#EXEC +../build-cli resolve -N testa -d 127.0.0.1:2223 -o wrong_os_name +#POST-EXEC +#EXPECT +Error: Unsupported OS name "wrong_os_name" is used! +Error: Check the following supported OS list. +* ubuntu-32 +* windows-32 diff --git a/test/build-cli-13.testcase b/test/build-cli-13.testcase new file mode 100644 index 0000000..b0bbc1f --- /dev/null +++ b/test/build-cli-13.testcase @@ -0,0 +1,38 @@ +#PRE-EXEC +echo "Assume that testc project has the password (1111)" +echo "Assume that testa,testb which are depended by testc are built and uploaded" +echo "For, work around solution, removed cache" +rm -rf buildsvr01/projects/testa/cache +../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -w 1111 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: * b +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... c_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testc" diff --git a/test/build-cli-14.testcase b/test/build-cli-14.testcase new file mode 100644 index 0000000..de41149 --- /dev/null +++ b/test/build-cli-14.testcase @@ -0,0 +1,9 @@ +#PRE-EXEC +echo "Assume that testc project has the password (1111)" +echo "Assume that testa,testb which are depended by testc are built and uploaded" +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Error: Project's password is not matched! +Error: Use -w option to input your project password diff --git a/test/build-cli-15.testcase b/test/build-cli-15.testcase new file mode 100644 index 0000000..523375a --- /dev/null +++ b/test/build-cli-15.testcase @@ -0,0 +1,9 @@ +#PRE-EXEC +echo "Assume that testc project has the password (1111)" +echo "Assume that testa,testb which are depended by testc are built and uploaded" +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -w 2222 -o ubuntu-32 +#POST-EXEC +#EXPECT +Error: Project's password is not matched! +Error: Use -w option to input your project password diff --git a/test/build-cli-16.testcase b/test/build-cli-16.testcase new file mode 100644 index 0000000..fd31b9a --- /dev/null +++ b/test/build-cli-16.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P c +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -w 1111 --async -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Above job(s) will be processed asynchronously! diff --git a/test/build-cli-17.testcase b/test/build-cli-17.testcase new file mode 100644 index 0000000..230f75e --- /dev/null +++ b/test/build-cli-17.testcase @@ -0,0 +1,40 @@ +#PRE-EXEC +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P c +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P b +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 --async -o ubuntu-32 +sleep 1 +../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Above job(s) will be processed asynchronously! +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Waiting for finishing following jobs: +Info: * +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... b_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testb" diff --git a/test/build-cli-18.testcase b/test/build-cli-18.testcase new file mode 100644 index 0000000..136daf7 --- /dev/null +++ b/test/build-cli-18.testcase @@ -0,0 +1,31 @@ +#PRE-EXEC +echo "reverse fail" +#EXEC +rm -rf git01/a +cd git01;tar xf a_v2.tar.gz +../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.2_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: * Will check reverse-build for projects: testb(ubuntu-32) +Info: * Added new job for reverse-build ... testb(ubuntu-32) +Info: * Reverse-build FAIL ... testb(ubuntu-32) +Error: Job is stopped by ERROR diff --git a/test/build-cli-19.testcase b/test/build-cli-19.testcase new file mode 100644 index 0000000..f48aab6 --- /dev/null +++ b/test/build-cli-19.testcase @@ -0,0 +1,16 @@ +#PRE-EXEC +#EXEC +../build-cli query-system -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* SYSTEM INFO * +HOST-OS: +MAX_WORKING_JOBS: + +* FTP * +FTP_ADDR: +FTP_USERNAME: + +* SUPPORTED OS LIST * +ubuntu-32 +windows-32 diff --git a/test/build-cli-20.testcase b/test/build-cli-20.testcase new file mode 100644 index 0000000..7f048f5 --- /dev/null +++ b/test/build-cli-20.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-cli query-project -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* PROJECT(S) * diff --git a/test/build-cli-21.testcase b/test/build-cli-21.testcase new file mode 100644 index 0000000..bd1503a --- /dev/null +++ b/test/build-cli-21.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-cli query-job -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* JOB(S) * diff --git a/test/build-cli-22.testcase b/test/build-cli-22.testcase new file mode 100644 index 0000000..6f4e6d5 --- /dev/null +++ b/test/build-cli-22.testcase @@ -0,0 +1,16 @@ +#PRE-EXEC +echo "Trying to upload a_0.0.1 with different commit-id is already uploaded" +rm -rf git01/c +cd git01;tar xf c_v1_1.tar.gz +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -o ubuntu-32 -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Error: Source code has been changed without increasing version! +Error: * Version : +Error: * Before : +Error: * Current : +Error: Job is stopped by ERROR diff --git a/test/build-cli-23.testcase b/test/build-cli-23.testcase new file mode 100644 index 0000000..9645f35 --- /dev/null +++ b/test/build-cli-23.testcase @@ -0,0 +1,25 @@ +#PRE-EXEC +cd git01;tar xf a_v2.tar.gz +cd git01;tar xf b_v2.tar.gz +cd git01;tar xf c_v2.tar.gz +#EXEC +../build-cli build -N testa,testb,testc -d 127.0.0.1:2223 -o ubuntu-32 -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job "testa" for ubuntu-32! +Info: Added new job "testb" for ubuntu-32! +Info: Added new job "testc" for ubuntu-32! +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-cli-24.testcase b/test/build-cli-24.testcase new file mode 100644 index 0000000..e858d32 --- /dev/null +++ b/test/build-cli-24.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +echo "This test case must be execute right after testcase 22" +#EXEC +../build-cli build -N testa,testb,testc -d 127.0.0.1:2223 -o ubuntu-32 -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Failed to initialize sub-job +Info: Failed to initialize sub-job +Info: Failed to initialize sub-job +Error: Job is stopped by ERROR diff --git a/test/build-cli-25.testcase b/test/build-cli-25.testcase new file mode 100644 index 0000000..a9287f8 --- /dev/null +++ b/test/build-cli-25.testcase @@ -0,0 +1,20 @@ +#PRE-EXEC +cd git01;tar xf a_v3.tar.gz +#EXEC +../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32,windows-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job "testa" for ubuntu-32! +Info: Added new job "testa" for windows-32! +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-cli-26.testcase b/test/build-cli-26.testcase new file mode 100644 index 0000000..b3c0684 --- /dev/null +++ b/test/build-cli-26.testcase @@ -0,0 +1,45 @@ +#PRE-EXEC +echo "testa, testb: build because of version change" +echo "testc, testa1, testd: rebuild with same version" + +cd git01;tar xf a_v4.tar.gz +cd git01;tar xf b_v4.tar.gz +cd git01;tar xf c_v4.tar.gz +#EXEC +../build-svr fullbuild -n testserver3 +#POST-EXEC +#EXPECT +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-cli-27.testcase b/test/build-cli-27.testcase new file mode 100644 index 0000000..b40b9b7 --- /dev/null +++ b/test/build-cli-27.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +cd git01;tar xf c_v5.tar.gz +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -o li_* -w 1111 +#POST-EXEC +#EXPECT +Error: There is no OS supported by the build server. diff --git a/test/build-cli-28.testcase b/test/build-cli-28.testcase new file mode 100644 index 0000000..04cc21e --- /dev/null +++ b/test/build-cli-28.testcase @@ -0,0 +1,33 @@ +#PRE-EXEC +echo "wild card" +#EXEC +../build-cli build -N testc -d 127.0.0.1:2223 -o ubuntu-* -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: * b +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... c_0.0.5_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testc" diff --git a/test/build-cli-29.testcase b/test/build-cli-29.testcase new file mode 100644 index 0000000..c927bae --- /dev/null +++ b/test/build-cli-29.testcase @@ -0,0 +1,38 @@ +#PRE-EXEC +echo "reverse success" +#EXEC +rm -rf git01/a +cd git01;tar xf a_v5.tar.gz +../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.5_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: * Will check reverse-build for projects: +Info: * Added new job for reverse-build ... +Info: * Added new job for reverse-build ... +Info: * Reverse-build OK ... +Info: * Reverse-build OK ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-svr-01.testcase b/test/build-svr-01.testcase new file mode 100644 index 0000000..e345367 --- /dev/null +++ b/test/build-svr-01.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf ~/.build_tools/build_server/testserver3 +rm -rf buildsvr01 +mkdir buildsvr01 +#EXEC +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Created new build server: "testserver3" diff --git a/test/build-svr-02.testcase b/test/build-svr-02.testcase new file mode 100644 index 0000000..cbedb98 --- /dev/null +++ b/test/build-svr-02.testcase @@ -0,0 +1,45 @@ +#PRE-EXEC +#EXEC +../build-svr -h +#POST-EXEC +#EXPECT +Build-server administer service command-line tool. + +Usage: build-svr [OPTS] or build-svr -h + +Subcommands: + create Create the build-server. + remove Remove the build-server. + start Start the build-server. + stop Stop the build-server. + add-svr Add build-server for support multi-OS or distribute build job. + add-prj Register information for project what you want build berfore building a project. + register Register the package to the build-server. + fullbuild Build all your projects and upload them to package server. + +Subcommand usage: + build-svr create -n -u -d -t + build-svr remove -n + build-svr start -n -p + build-svr stop -n + build-svr add-svr -n -d + build-svr add-prj -n -N [-g ] [-b ] [-P ] [-w ] [-o ] + build-svr add-os -n -o + build-svr register -n -P + build-svr fullbuild -n + +Options: + -n, --name build server name + -u, --url package server url: http://127.0.0.1/dibs/unstable + -d, --address server address: 127.0.0.1:2224 + -p, --port server port number: 2224 + -P, --pkg package file path or name + -o, --os ex) linux,windows + -N, --pname project name + -g, --git git repository + -b, --branch git branch + -w, --passwd password for managing project + -t, --ftp ftp server url: ftp://dibsftp:dibsftp@127.0.0.1:1024 + -h, --help display this information + -v, --version display version + diff --git a/test/buildserver03.testcase b/test/build-svr-03.testcase similarity index 53% rename from test/buildserver03.testcase rename to test/build-svr-03.testcase index de131bf..6038be5 100644 --- a/test/buildserver03.testcase +++ b/test/build-svr-03.testcase @@ -1,9 +1,9 @@ #PRE-EXEC rm -rf buildsvr01 mkdir buildsvr01 -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/testserver3/unstable -d pkgserver -i testserver3 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 #EXEC -../build-svr add -n testserver3 -d 127.0.0.1 -p 2223 +../build-svr add-svr -n testserver3 -d 127.0.0.1:2223 cat ~/.build_tools/build_server/testserver3/friends #POST-EXEC ../build-svr remove -n testserver3 diff --git a/test/buildserver04.testcase b/test/build-svr-04.testcase similarity index 58% rename from test/buildserver04.testcase rename to test/build-svr-04.testcase index bdad4d1..5a7d65e 100644 --- a/test/buildserver04.testcase +++ b/test/build-svr-04.testcase @@ -1,7 +1,8 @@ #PRE-EXEC +rm -rf ~/.build_tools/build_server/testserver3 rm -rf buildsvr01 mkdir buildsvr01 -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/emptyserver/unstable -d pkgserver -i emptyserver +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 #EXEC echo "TEST_TIME=3" >> ~/.build_tools/build_server/testserver3/server.cfg ../build-svr start -n testserver3 -p 2223 diff --git a/test/buildserver05.testcase b/test/build-svr-05.testcase similarity index 51% rename from test/buildserver05.testcase rename to test/build-svr-05.testcase index d15c978..634cb10 100644 --- a/test/buildserver05.testcase +++ b/test/build-svr-05.testcase @@ -1,10 +1,10 @@ #PRE-EXEC -rm -rf buildsvr01 mkdir buildsvr01 -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/emptyserver/unstable -d pkgserver -i emptyserver +rm -rf ~/.build_tools/build_server/testserver3 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 ../build-svr start -n testserver3 -p 2223 & #EXEC -sleep 2 +sleep 1 ../build-svr stop -n testserver3 sleep 1 #POST-EXEC diff --git a/test/buildserver06.testcase b/test/build-svr-06.testcase similarity index 53% rename from test/buildserver06.testcase rename to test/build-svr-06.testcase index d95913d..b0f302a 100644 --- a/test/buildserver06.testcase +++ b/test/build-svr-06.testcase @@ -1,6 +1,6 @@ #PRE-EXEC mkdir buildsvr01 -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/emptyserver/unstable -d pkgserver -i emptyserver +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 #EXEC ../build-svr stop -n testserver3 #POST-EXEC diff --git a/test/build-svr-07.testcase b/test/build-svr-07.testcase new file mode 100644 index 0000000..856e52b --- /dev/null +++ b/test/build-svr-07.testcase @@ -0,0 +1,9 @@ +#PRE-EXEC +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../build-svr remove -n testserver3 +#POST-EXEC +rm -rf buildsvr01 +#EXPECT +Removed the server diff --git a/test/build-svr-08.testcase b/test/build-svr-08.testcase new file mode 100644 index 0000000..c18ffa2 --- /dev/null +++ b/test/build-svr-08.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf ~/.build_tools/build_server/testserver3 +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Creating server failed. The server id is already exist diff --git a/test/build-svr-09.testcase b/test/build-svr-09.testcase new file mode 100644 index 0000000..0554f0b --- /dev/null +++ b/test/build-svr-09.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-svr remove -n testserverxx +#POST-EXEC +#EXPECT +does not exist! diff --git a/test/build-svr-10.testcase b/test/build-svr-10.testcase new file mode 100644 index 0000000..126e55f --- /dev/null +++ b/test/build-svr-10.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-svr start -n testserverxx +#POST-EXEC +#EXPECT +does not exist! diff --git a/test/build-svr-11.testcase b/test/build-svr-11.testcase new file mode 100644 index 0000000..1632086 --- /dev/null +++ b/test/build-svr-11.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../build-svr add-os -n testserver3 -o linux +#EXEC +../build-svr add-prj -n testserver3 -N testa -g test_git -b test_branch +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Adding project succeeded! diff --git a/test/build-svr-12.testcase b/test/build-svr-12.testcase new file mode 100644 index 0000000..69e3cd5 --- /dev/null +++ b/test/build-svr-12.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../build-svr add-prj -n testserverxxx -N testa -g test_git -b test_branch +#POST-EXEC +#EXPECT +does not exist! diff --git a/test/build-svr-13.testcase b/test/build-svr-13.testcase new file mode 100644 index 0000000..110677a --- /dev/null +++ b/test/build-svr-13.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../build-svr add-prj -n testserver3 -N testa -g test_git -b test_branch -w 1111 +cat buildsvr01/projects/testa/build | grep PASSWD +#POST-EXEC +#EXPECT +Adding project succeeded! +PASSWD=1111 diff --git a/test/build-svr-14.testcase b/test/build-svr-14.testcase new file mode 100644 index 0000000..0d920e9 --- /dev/null +++ b/test/build-svr-14.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../build-svr add-os -n testserver3 -o linux +#EXEC +../build-svr add-prj -n testserver3 -N testx -g test_git -b test_branch -o linux +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Adding project succeeded! diff --git a/test/build-svr-15.testcase b/test/build-svr-15.testcase new file mode 100644 index 0000000..7574a81 --- /dev/null +++ b/test/build-svr-15.testcase @@ -0,0 +1,19 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../build-svr register -n testserver3 -P bin/bin_0.0.0_linux.zip +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for REGISTER Job +Info: New Job +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-svr-16.testcase b/test/build-svr-16.testcase new file mode 100644 index 0000000..d2ade40 --- /dev/null +++ b/test/build-svr-16.testcase @@ -0,0 +1,15 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../build-svr register -n testserver3 -P bin/bin_0.0.0_linux.zip +#EXEC +../build-svr register -n testserver3 -P bin/bin_0.0.0_linux.zip +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Info: Initializing job... +Info: Checking package version ... +Error: Version must be increased : +Error: Job is stopped by ERROR diff --git a/test/build-svr-17.testcase b/test/build-svr-17.testcase new file mode 100644 index 0000000..8f72321 --- /dev/null +++ b/test/build-svr-17.testcase @@ -0,0 +1,13 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../build-svr add-os -n testserver3 -o linux +cat ~/.build_tools/build_server/testserver3/supported_os_list +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Target OS is added successfully! +linux diff --git a/test/build-svr-18.testcase b/test/build-svr-18.testcase new file mode 100644 index 0000000..21beedd --- /dev/null +++ b/test/build-svr-18.testcase @@ -0,0 +1,13 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../build-svr add-os -n testserver3 -o linux +../build-svr add-os -n testserver3 -o linux +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Target OS is added successfully! +Target OS already exists in list! diff --git a/test/build-svr-19.testcase b/test/build-svr-19.testcase new file mode 100644 index 0000000..3146917 --- /dev/null +++ b/test/build-svr-19.testcase @@ -0,0 +1,16 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../build-svr add-os -n testserver3 -o linux +../build-svr add-os -n testserver3 -o windows +#EXEC +../build-svr add-prj -n testserver3 -N new_project -g new_git -b new_branch -o wrong_os_name +#POST-EXEC +../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Unsupported OS name "wrong_os_name" is used! +Check the following supported OS list: + * linux + * windows diff --git a/test/build-svr-20.testcase b/test/build-svr-20.testcase new file mode 100644 index 0000000..460e079 --- /dev/null +++ b/test/build-svr-20.testcase @@ -0,0 +1,20 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../build-svr add-os -n testserver3 -o linux +cp bin/bin_0.0.0_linux.zip bin/bin_0.0.0_wrongosname.zip +../build-svr start -n testserver3 -p 2223 & +#EXEC +sleep 1 +../build-svr register -n testserver3 -P bin/bin_0.0.0_wrongosname.zip +#POST-EXEC +../build-svr stop -n testserver3 +sleep 1 +../build-svr remove -n testserver3 +rm -rf buildsvr01 +rm -rf bin/bin/bin_0.0.0_wrongosname.zip +#EXPECT +Info: Initializing job... +Error: Unsupported OS "wrongosname" is used! +Error: Job is stopped by ERROR diff --git a/test/buildcli.testsuite b/test/buildcli.testsuite index 06947c4..9904eb3 100644 --- a/test/buildcli.testsuite +++ b/test/buildcli.testsuite @@ -1,2 +1,30 @@ build-cli-01.testcase build-cli-02.testcase +build-cli-03.testcase +build-cli-04.testcase +build-cli-05.testcase +build-cli-06.testcase +build-cli-07.testcase +build-cli-08.testcase +build-cli-09.testcase +build-cli-10.testcase +build-cli-11.testcase +build-cli-12.testcase +build-cli-12_1.testcase +build-cli-13.testcase +build-cli-14.testcase +build-cli-15.testcase +build-cli-16.testcase +build-cli-17.testcase +build-cli-18.testcase +build-cli-19.testcase +build-cli-20.testcase +build-cli-21.testcase +build-cli-22.testcase +build-cli-23.testcase +build-cli-24.testcase +build-cli-25.testcase +build-cli-26.testcase +build-cli-27.testcase +build-cli-28.testcase +build-cli-29.testcase diff --git a/test/buildserver.testsuite b/test/buildserver.testsuite index f908bff..d3b6e7a 100644 --- a/test/buildserver.testsuite +++ b/test/buildserver.testsuite @@ -1,6 +1,18 @@ -buildserver01.testcase -buildserver02.testcase -buildserver03.testcase -buildserver04.testcase -buildserver05.testcase -buildserver06.testcase +build-svr-01.testcase +build-svr-02.testcase +build-svr-03.testcase +build-svr-04.testcase +build-svr-05.testcase +build-svr-06.testcase +build-svr-07.testcase +build-svr-08.testcase +build-svr-09.testcase +build-svr-10.testcase +build-svr-11.testcase +build-svr-12.testcase +build-svr-13.testcase +build-svr-14.testcase +build-svr-17.testcase +build-svr-18.testcase +build-svr-19.testcase +build-svr-20.testcase diff --git a/test/buildserver01.testcase b/test/buildserver01.testcase deleted file mode 100644 index fa32179..0000000 --- a/test/buildserver01.testcase +++ /dev/null @@ -1,11 +0,0 @@ -#PRE-EXEC -rm -rf ~/.build_tools/build_server/testserver3 -rm -rf buildsvr01 -mkdir buildsvr01 -#EXEC -cd buildsvr01; ../../build-svr create -n testserver3 -u http://172.21.111.132/testserver3/unstable -d pkgserver -i testserver3 -#POST-EXEC -../build-svr remove -n testserver3 -rm -rf buildsvr01 -#EXPECT -reated new build server: "testserver3" diff --git a/test/buildserver02.testcase b/test/buildserver02.testcase deleted file mode 100644 index e490e82..0000000 --- a/test/buildserver02.testcase +++ /dev/null @@ -1,18 +0,0 @@ -#PRE-EXEC -#EXEC -../build-svr -h -#POST-EXEC -#EXPECT -Usage: build-svr {create|remove|start|build|help} ... - build-svr create -n -u -d -i - build-svr remove -n - build-svr start -n [-p - build-svr add -n [-d -p ] - -n, --name build server name - -u, --url package server URL: http://xxx/yyy/zzz - -d package svr or friend svr ip or ssh alias - --domain - -i, --id package server id - -p, --port port - -h, --help display this information diff --git a/test/buildsvr.init b/test/buildsvr.init new file mode 100644 index 0000000..5b294f6 --- /dev/null +++ b/test/buildsvr.init @@ -0,0 +1,32 @@ +#!/bin/sh +rm -rf buildsvr01 +rm -rf ~/.build_tools/build_server/testserver3 +mkdir buildsvr01 +cd buildsvr01 +../../build-svr remove -n testserver3 +../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://ftpuser:ftpuser@127.0.0.1 +../../build-svr add-svr -n testserver3 -d 127.0.0.1:2224 +../../build-svr add-svr -n testserver3 -u `pwd`/../pkgsvr02/unstable +cd .. +cd git01 +rm -rf a +rm -rf a1 +rm -rf b +rm -rf c +rm -rf d +tar xvf a_v1.tar.gz +tar xvf b_v1.tar.gz +tar xvf c_v1.tar.gz +tar xvf d_v0.tar.gz +tar xvf a1_v1.tar.gz +cd .. +../build-svr add-os -n testserver3 -o ubuntu-32 +../build-svr add-os -n testserver3 -o windows-32 +../build-svr add-prj -n testserver3 -N testa -g `pwd`/git01/a -b master +../build-svr add-prj -n testserver3 -N testb -g `pwd`/git01/b -b master +../build-svr add-prj -n testserver3 -N testc -g `pwd`/git01/c -b master -w 1111 +../build-svr add-prj -n testserver3 -N testd -g `pwd`/git01/d -b master -o ubuntu-32 +../build-svr add-prj -n testserver3 -N teste -P bin +../build-svr add-prj -n testserver3 -N testa1 -g `pwd`/git01/a1 -b master +../pkg-svr register -n pkgsvr01 -d unstable -P bin/bin_0.0.0_ubuntu-32.zip +../build-svr start -n testserver3 -p 2223 diff --git a/test/c/c b/test/c/c deleted file mode 100644 index 16fc679..0000000 --- a/test/c/c +++ /dev/null @@ -1 +0,0 @@ -ca diff --git a/test/c/package/build.linux b/test/c/package/build.linux deleted file mode 100755 index b774e2d..0000000 --- a/test/c/package/build.linux +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -e - -clean () -{ - rm -rf $SRCDIR/*.zip - rm -rf $SRCDIR/*.tar.gz -} - -build () -{ - echo "C: clean build (no dependency) ok" -} - -install () -{ - mkdir -p $SRCDIR/package/c.package.linux/data - cp $SRCDIR/c $SRCDIR/package/c.package.linux/data -} - -$1 -echo "$1 success" diff --git a/test/c/package/pkginfo.manifest b/test/c/package/pkginfo.manifest deleted file mode 100644 index ede7a91..0000000 --- a/test/c/package/pkginfo.manifest +++ /dev/null @@ -1,6 +0,0 @@ -Package: c -Version: 11 -OS: linux -Build-host-os: linux -Maintainer: xx -Source: c diff --git a/test/git01/a.tar.gz b/test/git01/a.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..be4b4ef3a53a61b24b8f2161af558b1acf77c3b8 GIT binary patch literal 14112 zcmV+*H{Zw~iwFQm=fzI|1MFG}IF#$#f5{YMN!kyGA!Li0cUD_jLxZwJ5jCqUV;O`{ zwqzN}l8BBvqU=JEC8{ZjL_~;+x#O~^dmtQ|45cC zu=!sIyyu@zq0SHYbNF)pK`aK%=6@mZA^#o(yLmB!|Aqf!LAL)d2tMSWPNBJY%)=S* z<@}>iC^r8KfG+W@BH=5;EVD<%pCt{EDFx%eO*!kc8Z@l+^l8ZCd-fo`O_?-W<-2UVK9|*GZ zzXgFc$%Rg_f%4EPt}gEO9xikrstcV)@YqM8yW6`s!_;ALC=b=%iIp6ZDXftP-ID_4 zArk0w11iDEoi%cDu_HJ+v5pW=S2BT>LjF3<`dvy1lX#a>O0brpQang|S?}dPIB?`t z{w)8LgYtONomf-tD0I5L$6gnA4^V~m-b0B1wSM<^6-?sYStapS{_fcwe^&y@kwV@ur>w8P#^?N>b?$%M|3x6#_dgZ{>tJ)#za<{##d0$h zm@9?iL?OeVa4ZD}f;b!;i(|1zAcJsaG?juTQqee!G64a{u)f6Ls91LO_}`58{_jF` zppZP~=`Y|fum6xJEW7`|Aoz%XlJYmN|50!2e>nU8=Yrsm{NvCdh$T`m1QLaaKu|z5 z3QH#AP&hahgHlWP4Wb zaCYYP8w&lCr%6PB3knIZZ4d9y56hJ;WiP;p2M27|z0aRe%6F6qahDM$5y&!b z<8gTV{-TYSeAn|?bUO=&=v6)CxyKBAx--^duWZYbm3z)>4Nh?RFYBgwbDl-%4t{H$ z{hRQS|Ial8{x97BLt@$Ye-{LQ#{b)n5E+3Wlc*Fdk&33Gu_z*u1cIy*00}1|;6y5l zRRbt1qbL}(;@s>{ukq>lkAR~PAMp>O;8-j>{x1Z!X=>wH4^GxcDPF)kB|Qou5De!7 zCMqO+rJl4%-Z0(%QvL>_F>ssanyKjw(}RsKoJ+IIla8O0?J>!d4%6;y=L$gyNe--M zFri5jmyQovWZ7K|yKyT~D{-XSL7AUZ9|b`P9YTSDaYc%q&qpnK(*3(lsRyS`Cemxf z2i+1bHPb4m1MVw|NKEm<1047Hw>lh@OetO3o~0}Qt3+4fe7X}U5~`+8aRxLzx7*6~ zMo?|DmW)LuXZ0ThJT(~Re>0K0B2q>A<{>%NR#~~TiQ=R^iLu_RM+~%vOww^fE~ zRo5B}LAa_@JblJ(XA|KL8~gKhb$=D=8jKI{vb|y}3omRrz|4I(W```Zrkd>lM?9Ta~hrd)#ahM}{w! zP;7s5aHR3ZC2p=&+aB)W+IH5)w(|HUp@@5{UpwkN}WYJ)EA#TdL* zo*dULeYqw=Q&7kb+`3&DiL*hQk6`kXZ#$ms8SP!?SvVo)xmw+8=XjcNSE=IC!HT6K z;y}AFJV3em>Ae%P2m;fO!~3yL(L0$`S@t9d@lCy|Zd!b?Z4-}pUg5Yir>;mm4pLIX zvZo${(TJqWk$WAZ^aFmn8BShGcB6*f6F#qOv0M#l&KS@wFV2%+FT-T4^(Q{wRVZZ# zg>Wt&Tjst&zsSU6yyl`~cgM(@lh_NoyAPuL`wukkPoX6ZTYMFhhz1Uc~|^`J;;~$#5r&mU(sQr$Zb*lVxenEkvoDl z)3bA@PoJ$+bR4cQvl)A>ckQ^ST}MNrxF)Y@+~^Z&K(6hsZNURoY2%&QA&YpBR%z%eN?&iws{o0wT~6G zT4E0*kXqZs9(tV^CmrypFvNBVKh`ep9fL>&s0^C31RBn2DVgG|py}*?D^i{2oJbl? z{>Cwt+o-Xq{(vycB*P30(c0duv2(DYviwf)LzX$U=!ux#U~Wt>ymDUJ)7(@k>yhO6 zHdv6)NCv(rc=hcFP7ZPLBjHKs^yr#}Ot+hgi`*1KqcOS3m2Fdf858>z#%Bfx8%7T( zlo+>85}T$IWxTn^xw!URR%vS;yVJ#ZN!iFaq60ZL$D9#N%8W_!(&v~0?!0HZ#=#COJ|%?_2Vl}`%!55;t*+0vnLbcrU#DF zGZOpD!!|Epd#F%+8G$Yk1M7*oztb!(c#CQ5oq_8t`@UW#Bz`8QX%%1JC8`}LZ7k>j z6A)!WO#Q|wou2I-6}wWgB|9G;MEUi*{lcXYzY{ijCgRU5E1=6D6Fbb7>l`kxVT@ls=K>_*L}_6jNB{>zeZb|XxlXH zGg?eWQ1?qO+4dTD;JQ$Kq~*!v^zNo4_k<83NGVwmc37agNiSdT+>Ok!9C0ic^)75&_jD3PlLAhj z4qUr~H_^8wJnzbsq&Oep}w6!O9|p}%ckU+#9jK~r6Ay?pMT($0!$ z^*H#!jm;DNPsMbceA|YSVqC&STZMA<2&{E zy0$u_`Z`1W21P>SgM^|(65c7+wWYCc zTA^V&T6K9Dhj>!*C1@6oGxB>I%S7m|#DG`KS|ZTTC$RpXZsES;Xzx&S$01(xs+$8& z@~Wvf?-3jsf33D-hmek6Y{(I7W4DC2T2(DK-#J$Gj5e%-YtFyN1h`i~h2n~qH=C{9 zQhX$T<^|q;SnN$mSsI^vBu4l$KLil2mrMD_99dJ>%PrqGa{utl)9skZ@9q@Tmz|1a zZP4O6U{VXleSNQ#vZ}G1eU*M`BKn|Kmr3X)-DLiUp_~A3t(?%;*PO*=ALmqfTQj+c zn6w!WMo+d{`i*ixi)0FZ4*@@_7)|!)I6wDJ8n|&dK`$tTXdG6s2ne{zlRa8XCQjX8XP;S~`4`Wlyis8+HyanQbS=Xt|4siYdeLHWy*z|?6O z>94PFC*^%1tDsKGR>gkhBNN9>s8iBvyc}D28mu;Piwp1$z zH=f{ikGnSazFI0&($PCyS%9bXhTmSV>v}Gibgs#*ZjW2(iPYHAAAhbsGZv==IdP05 zb|o?*+a1%}*r7U4=-aQ7bh!q@>xh>?k{O~hqUcI^pNg>aHxM?&l(;+Ry zpmcJXlv-wyK3AOiYvVVJ0#Dz&(l5Fa+@o4^%uw6G!*Kn@EK! zDzs9E1Vxa9gd+hFss=@@iXwh(K`NVM11m{3=HT$agSKkbR@+*YdSBIA@xJiZYHihe z)Yi7v9twE(lYV%Xul>K--9Qp3M{D8x*Sw$a$7Xlt&6}Bb&D(Kje`@w{ozGo5<&#AP z-PT9#Q{0JO-)+BI_@MpDCCz6X>hj!;;j>>a32=W;7E^vVC6A{{+?jRRWpaGZqMPXt zPwpkU3RB|8jGfW%a&hZfk9dkZt*#WiJeRZR*OL_sbEkT){3-T&SERgHJYrS>Z}^w3 zrx&~UR6RUv`|w8VH*Rtd=j9#|4*2PplY8H-_(fUxW5u}@#YruDz7{LyEzRTeie0)x zsn4JkkSMDp@hvZLNb9#lKzKlqc%SpMXLdKE2K_vdC!EwtF-&{Ro46GCP19y!nhySY z{*O}U)X_#A?|;I=L&6;Lf1&blnKS>_81TC}LB(V9f3<-NSEfGL{131NV9Q#t6^E^D zu#E${0`eWOZ9UP=0iObF=YZ}UdeneV*8|)CN$k)-^sEIta@ffMJ3F8khu#kO4B)em z8M^@b0HOw;^wyKr z8_>MpQtrN9;Gr3maleiK@&hX(rn$Z{=0I}5<%iqirn;nBYtHwM+`b1*J+v=UHt0d zpiARFn}WPnz3`v-o+T~H;|0%^w(1pgeb|bGGZThio_~nE;vM5c z@tT;9UHZ5zRN*8;4IVdM}C#4&+^KXCj()rEf&&FzoJP@}$ppEU5>AFdg7VzvZd6vtm zxjoWa1}stJ7$biyiybzyR6YB|&@Re1MIX*l$C?8_S@+;kz`lxvw7jFd#m24D+aq%p zcngTAz+cn4wFthqZ`vD&77l%FLePMW7Ja^?)x_G#2j5Qr^twmBFz?ZVtn~AFYdwD} zHzzFaGyUn<_sgeV`5vWhJcn|im4JCo@B>%-GC@;~3-rG>}XuH)9t^Y%M&?7@~z z1-kj(fhTv&OyO6wb@lS13i>SF^TM13>whS?n-R6ny=VV}#mfh(ufEq)uuysY(_wSH z&$Y^2w=w8O;Na4^vZD*n?OTjmc&u0#jgILH6+ej!@84cn60_(XqU+a-Zf7i$PwH^> zqgJ~Os{+!8ZaCg;x7V0Oy6t5}JJKf%+OYaqYLR$lgy)iF9~?ELCxqu7yzt_>TUVb? zTO1#@)i0ZOyQC8j97*hRt$ase!MVM&BU+uG)ja?5 z*fUR0cAK|xU+;=jB@>q~>sIa)v?J>@!Ji%-dR&kc!$hbd1?nrd03%HPLA$B zX575j=D!>Co6FRfZ*SyZK3_gynQqvba>13%cYWd>++6$7%^x0_g~8W`2@du-t?l(i zNZPr{C)Ae@uP5USW4=3>;UhKd^gHFg?5)dJUcG-X+;`NooButa->IEjySDHNzL1$9 zblc?Pek5hoqlFiqS#&jMh4<>Vf$iO!#~z-$J@4Fj|Ftezr{CG5-Tupjjm2f3d{3n& zL=NiICfVcjA3_ynJ>0Tp%(?m^#Xr#{Jna0M)_!YCe|yHfXMCCH*ze~G`Ysrw&*BB- z%a#9&D9|7JlbJ*zh5gErZ`f zAxt;}*Aw9$_^;N*;MT!sx5A)NRXXe@)@>!I1n(Do82 z4{=1BMEDJSxZQ&C$xwF&(7Fm?N404q#+^B{eO)Df7kx+i>36g!25T{?Avl`^Yzo^ z?z&MsXA0Z$o{kCntf{-~1RHSBe@d^R{}=ZEg-M;?|7a9&-2bgjCFoy*qD@JLr3J{;p4oV;~mDtbfw;jQK#XBKrX z?ePkAwb#^6b3biH_Gxxg5;^2wxwluyuQltw?d2(*uir|!*ll}}UUhZxjX?VOg8k(Y ziIJZd4gLDF)G?VW?@IhO^51QL#3wH5_u;;8Kj6J8ji;J=Xr2gg(0{3_$?ZS8{&SxH z(l}6k{$(MWP<6PR2n$t);}1ZD60rKyka9H`e?p^{D5Ww9A#X?xJw8xroc z|BV8b>%R~}Qhis5YXhp@9E*;66 z1tY_^TON5Xo-}L7T>65dV<+_oP@v>yN%vB6&FW6`B?JAo-5Qg;;NZi+bDHhP4oq#e z;e>B%m-EfTe4D!aPdqs2zw&?O|NqLJ>%T^U>hm9|QA*U|Qc@ERUO)q}xk@FAl#}2V zR3SrAzwnZ0dn| zLcl@)LqeO}{>#|!KRDz6MuF<{uTqif&`7l=Ql*Xz4O0_g{Z>hDnb>i zkwnTG(niDVKa>9nkvPBq(5l(Sx3ICR>LW)e|q&Uk5A zEAbC6?C+{y-FwfDP6-n`oL!K3(zp2dt%Hh}hE1R1btOpKdwJO+P5zDk@1HO9<-aNE z7u%+Da**HBqo==74OAmHZ%JJLxJlcF1aByOYx95B6y3P_)|%i;*@3?fzc3?ZH+7{; z(VR0cO?D6Ri+j##pU>&dR`=2s-wfv7n z?$rNAf!O#&1&`gg+cM#3$#b%0t4k6--kr=oZR5h5_0)JCZ^|kjulY3=dAyqTF7fzS zj;M4!Z6vL<6ONWX=((+kkN*Y?{CF)(uiYUBdd+LvTg7uBp#D|2w!69w$A4Xk$yppf zQyuExmd(taqxIgHuOZUkxJNA~uc&>#;B~;e*oky9SZSfOYO$8mTXHJ-s6VM)kZhhp03N?n)XZbysUH$nd$f~tXN0rDGg~d^LXY$p5K~Xw<4we z#6AaB9npQf?)M9iYyLU?S0oG>5}!1>e{6ixkP&f@i#P{$>3^6s)bahF)Vcqoae&|p z(J(DZm`FtG&7={TGf1SObvnw7bfn1y5{Wdl5#`brBT{7$`gGDHMuRCmX+n79%#=PI zNZ?NgBY&YkTxhjONg~z~sDlupR|6taXE5hhQY4hL(QLA5OAIs;^oaYeH}~~mAyTjn zA~TI?vPI3Lx)(e`sD}lZ7J&33BWWVd2%lGkdgE`E$B4yZ0j^}x1CQxs0AI)#x~fSn zi7P`@QVjgN3_xA_53BuZ`tOYY8wcvE|E{i_Z}w-Mvp=FWH66{-)ctWn!+_KO z44TgT+YW%QcmB8P{2yt!GymHtU^o8;&}0!8kedjdK}*(6pdL`y{D)LO|5Fy?eE-)d zAoLAZQu<(123{hezOYcsVJ047i9jZmkusQJoko}uWkM=Ki;spOGU zEt`6L{^?++|Lpl)k5ly-s6+qd66^b)O8pO)g*oHDMuGb2KaG@HT9s)+7CoWY(`Jwo zH8KqPgfZQsg9;|**kQ&Lm@){d zh}Npv3q6_LK;yxRt$?*T3g#q3xDYV_7mRF+fh{`QhJ7Z5l!WOhXrLoO5PROof72RQ|xm1n{d#dtJ3{%Lecr z1+aiTCZ8S>|h$bKbRN{bK;*%yqRVf2Zcg(p4(gb%v7&$4!nu}u>u~>}%1c9yd2VJ&C z!DgDZTVSNHmlN~Hkw!XNAgLz^Sh24X>1epdBy5D6eKZ=g7R|QBCz;b&Z67dGCsGk+ z5z}>%*~)(aBj?-*TQw`l+8|Sz8EjYPa7XNUf&+ut&cP_V3c){$>>MQAG-c36^JuHV z@*B@x358WOCPFc;YmT}iK)%pRd~U2wCtYnOGzn~yW2-^P&#u)VJTLIv-Z#KK8(CrJ z+m`Z37zKYUtU9k+=jT<{X z?6`4a!H$^iP*mz)odXeT^@yxfZa|M~<&x20%%bDG+HzT`>~>tD?%fgl9xe+aJtGXK zRi;o30k<);3RDCKab1W z7$;_N!#LbtVE;1pbhe&xNH_t{u6(djf>EkcI(WjAus7!{ca_XJc)9_Uo-$=X?8+37 zI1?{V2H59+jpW~u{;ezjgTFK5nEwq6l{)i3jREz||74Ly@VKxPW;4NrlD3!~x8+n% z|8U_jllX!4KxOVnjF@naZ6gD(9ib{?c(|f^5q7Y6hmqN0;_?iX$z)+nvmOx!9E2Gu zZ2YWyiYN_^(9KMk#_Sj}nQ4Rl7Ndrxq$`yHXQaWSu=y-&gd-AhL&9u3igAc?SkD4c zBBeK(qj3=~Iv1jNJ!Pf{EyqE=iy2$Oq@pOMr?}rHybNMHpGCl! zD%kJZ-s+#ywp(={{R8sHb>LnXrzt3P+zbjI@aF6e8X7T3kkvv ziv?^YYgHy)rvklE5qQW!+F-JECPdi_e>QE*6jOeaS_rvnZ2QRVwWV$?}P}AKKI4R1rsStPRD#@bsu@EZs~B4f>$A(u7V`a?7Sv zEHJt0y&T9QRJ%ShN?cW^D#JzQkSb)ITM@JSAI~t;QsqonPNhiX3J|SnX)rKu|JUBJb~kNg_p9|QW*u*1gJ1G92_zIkw`I45 zJ&?9%4-k;0u_Izju4Dr#u)lrpV@4Xw&Wk#R?rKg$ZF!z|?tAVX6-@*wlC08d)$*^Z zEt<&lK&fyG$AKpfuu<2QQzK7-hifMIhhL=x7E320dg&P!d$3_XBj;tvc%qG3Yz8 z;e~F4b#(Z$IJnucCMS+R&iViUXj$O@4j$i4otb-5o5Z8FNr21ZzxvJ0`#09Ve|ERuTk`*P^8BBTly%Vm#eTmKbrhwcRatk@ zXB0|VA$Jeh#Hi;j6XLS3a0Ry@b?-X9!j;@231eO47dOQ%40=ZGO#Hl{;SD6Z>k?PU zGk+9FuNH#24nQW&PkF0eVm!lwi?(fh&YU9u#S1)|0Zg#UXc{r zjpOH{@bZ?8|2Dks3eEF_^2r>0(-IFao`-hCeEJHs>Sg3XEWlMZ--5-LrVpzA@Zek7T+p97 zqE5oNRX5xO+Nws|F{teG$TclScs89!pi-&v!)?%EfovkWZ6=c zgA|OsN4nI8GdZrwX);8|*5HwD2PT_4o_lx=&Iv|^s2$QlG($g%XF^$yu?Xin^r46e z3}A=M;=LkhZD7m*;2c<^xbeOc?)Va7#ZdW+9rL+00)X# z)YucJ%K;EaUV_VDLC04yp{tCQF3Fg9rgC}^W$4i9NTuvVuw7#ibYc{SY*p8jSo&7A z)TYRJ!GNw-EV^$jUQ$jABVvabrs~sscW2iLJ&~0|i*)IN7s?sMiHNzQZ9b z$}YWg2T`N?5#uDrApNQp$~9W(9^kc2Pw@wAD~d0IqNt14=LT~m+xgnsD)2;LLf?EN zN`zgvZ;SnX(KO~D#I1@jB1pfaH{eUN&_8uq(*Gkl08HMX|JSy=OZtDWW9Rn&M#`f6 zKMbd5kv}?_iLzS}sD@SX-yQLfdEkfQQ2gLboFFug{1c2RCJsY9?l%QMkpm~Wl8V2h zuqTSwjK!O9z}LiK1eA&ZT*yEqGXi=IC>o#gLMTQbr2zDi5zHC$BE7aC(M4Fdz#c#< z0{a8=BHp7`B^x**hwn|}StK1!g^a=Y!6Z-6R$xWw{lF=q(o<^b1}aN{qZN6zo+x8H z0mXB^qg+UQ%pO`;-8_nLr7B%dPa88aS!#h{^bR$EcS`?axP(*RP4iAkUXfphb7n}N z0kK1_+HDLH*w!;6D30W8W=yb}NL)vQgmj{JWRgWP#!OY;n7B^Ona&4eN*53?VG6{w zEcI+3N42nfm9t1p8f1zJDcnN;YfFXy(Q@{SMpOjmwsNQ}|3*S9KAQ|e5Q{ZTbHHfY z*B}g(AGIm&z=Sc9j`A~n%tepp(^)!cPtI7{Q5>$3_(mg;v^FMi?_LAbH9)xG|1KVa zA_viPImuLQnq5A%<8HzTdOR z&#zQ8?h^K7JXZNq_bdV!pbY`5_FKBnjF-CjIK(SFPV|gaHN!oPF~qSNV>rhlQsm-U z-m?H8ul(Q@f2G}`AZfq^j2i^Boz2en!$6X{bz3GL|+OPt%3`MRM zW?rdmSe_Uz^it$B0GJ|Zfpu%tsSAb4nW7Q^Vd?(U?|*sTe0fJb2g3~kdrQ|q+g8~n z$)ZynqLDN+*aQ7sbyNPEG2;q!IB}*(wS_|Cxct0X+jVNgm$xgTf%OYz5Lo*M!mi-A zPa>X<{aLy3A^^LBr)R&UpU^{(bChE_5#^-IOrM~aS4!M*;W_tQYSH>u_T73}sc=7l z3gy3Ywx%c<^Z!5t)(!wlpHx+ zfc;W|kgCDocFOjO=9G%dY-TYon()Wk`h&%?Oi9kZ6lW+$b0_lRl|uZ)c`GT((Mn~j zk^d0McbGp=$BrdMvk#FZoH(9iF1(_MJ+RDfi6?<8v*HHz7Ij-oQ7ychd?H#xCWp4F z!cj8n0T>hmF3Qs9+c^A4x-;OtfKYy*zk%{{-na>~b^!($s|os?IYKfSi!Tg;`s$uu z_QfE34J6YH-MDQCN=~$Q!w6@A?WGfqePq>uiO#8vtLV0o@0Mo7*}CLgLb#A(LmiMv z^{3jyq$g+C{>bA&GxP}vfyDc-oal`Bl3Kw8N5&5lqyS#19Ewv3<}BTyth6N&`-m6} zwqJo+l7unWN~{%=KNuMn?q8`>wigO>4~D=5TaCt6h})gsQvxp(l;je5VL2p*n#BQzrnu%XS6RE6_)g!8GKz1V#!KPB|@47`E`=KtLn%#$@OZ z?G7Tpcs8HmqEGaLe@^KyeNb(FrX`P1HQ)g`mp$2D=%~BN|?jTvb6 zd?V#a)aC)T8LISqc8jm3I@MMSZJdhY#<(v0O7{^Ui7>C$l9a|dq+XE&WO*Z^8fe(a zie-lDvf_QH`b)e9U|FKm;C@wQOoEmkIU{L3SQ_{U1@vS(2Yvvo8} z8mdqYHa-CA$QOXJt0GmojRk_fs?H3*A+a^h$va6T*i(%!u5 z7v=voj@Q;d2E}+>bpN-Vz5lV(%Fll{QeU}qE~EcutDm9&R;Qci|7@gyB;Q++l=!8mm6r{bb1*v5RM2_F# zmU;;s`MKy+KYa-tn#VdIt*+AQ9eojyn1nGpPr*7ntY*&R+4#h}ojJA&FD43ybT<@@ zijEu*PeClo3+eEa>~$|_o#OKfrt#mZKM$Rk_~AdfV9OLm|Ko6UW3PYtEdG!0f6Dp) z%@=e3Ne#S#F|NG-`2J6R|4$$Jf4=`WUzYY?-?X~MF|NP=-Hx5p|3=EW`yWRB$PdHnQxC*A)Z<^S0E`@c3*uKNB*(Q3P{^xC`LaM$&A z+a1^G*j~HYvpYMzmfPvOj@uqKcYDGMgVBkU;>-bs{;v8~$8X=p&Lo}(qdI8r2f}W* zciO$(9eYRIZtgal1$CMA0HaIe6YI(YwukOcchKy*c6&H%HQUZ$r@yn?Y!8~!-r4E2 zy;iR?7<#+co>`~0dS+jG&(>L%=%3wyYaQeI`hOSx%lZE%$~pVL4z{0^%lLo4W$^!Q z59NQW)6e<;CQ9>)MO;}>T!+iHc3a(kQ*l{$90qbv=P4|`S@#y`yZd%t!}TK>wlXl>Hbp? zZhhx?9szu0vP9pn!fV7PvNsUOVHlrT(v4Jb3gNGXwVZ2pS)q zy82WveR%NmqaPo>I=cV#`|plkJ$d%20rt4lunLn?&yPfHDoUystKd#WEuF5D{L=U; zuG{A#b`|GC@h=jZ<$DeI>Hob3NxC8huA=?DWn>JumMhoEw= zZ;VUrzh3tK&n~KqIsI>-{EK&Apxy9`jAFcBYfm)m&3fDV$-#GL`GJh!(ae!==9njU zmp|1{oCSlVD&F}1Se%@NQ0p#!uG8mCCAMhbhtp-7rn+GK+!f~;KjOf8Xj})j$H|h8 z_2e0RCI!}dfDEXH_G2xKlc&$=sd5?p eH<$eXZM&1-|CyJ(}PK7y$rk?nSQv literal 0 HcmV?d00001 diff --git a/test/git01/a1_v1.tar.gz b/test/git01/a1_v1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b363c5aa0e951b66bf458bb34915e49852e0c354 GIT binary patch literal 9123 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 literal 0 HcmV?d00001 diff --git a/test/git01/a_v1.tar.gz b/test/git01/a_v1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..049b900a7fca767f934d156b12598b67b77d54e3 GIT binary patch literal 8555 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 I0fdO~KXEoHOaK4? literal 0 HcmV?d00001 diff --git a/test/git01/a_v4.tar.gz b/test/git01/a_v4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..25f7b3bb328ff58205e9479e02d386d178d1a18f GIT binary patch literal 10958 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 literal 0 HcmV?d00001 diff --git a/test/git01/b.tar.gz b/test/git01/b.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6d263a1719f9ae22a8c9103fc94be121a467da1b GIT binary patch literal 13951 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

3e~ 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 Lb`9W&;NkuU=IV+P literal 0 HcmV?d00001 diff --git a/test/git01/b_v4.tar.gz b/test/git01/b_v4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..60ae7c15a4d8f28f98ff893c541ad60cbd675882 GIT binary patch literal 11469 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 Pf?+=3|B?oY!hrq{&b+sw literal 0 HcmV?d00001 diff --git a/test/git01/c.tar.gz b/test/git01/c.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..92ddfb7edd851c38672de29a982aecf360162779 GIT binary patch literal 18834 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 Z1r$&~0RN)tcGMAJK2g7F)LYyK~xAu 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 R1O!AE{tXNfZ2kc7001I#6~zDm literal 0 HcmV?d00001 diff --git a/test/git01/c_v1_1.tar.gz b/test/git01/c_v1_1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..01177c7f367e75347d1f56fb4afa9e7741e858ff GIT binary patch literal 9680 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 literal 0 HcmV?d00001 diff --git a/test/git01/c_v4.tar.gz b/test/git01/c_v4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b108b4ae85abc39286ce617d8624e22e399ee9c6 GIT binary patch literal 11979 zcmX|lWmHsOxcAWAAfR;rkp^iT8bPGH8v*GqM{8^n}m;2te z-t%>z56^n`FE@QGHYi7*Km&w!Bx(GKWkI)nf#KJCTw+G_iI{ztrB6$%RX+OgS*Pi3bOWr9&YAB;@Us$%>)l8#7ETxmq&AL4Y%l3PE}#iLf% z?FF(aPWTLNR^DXy2!}D=uf?gZ9bDeYqHkP1x;8jfvYpYJ>Hpsmz5AY(U&7YWV|8^y z95ujeZF9f2_cNG8%gqO|R?%pO1ho2C04%$lvwg{1Y0Rdjk;3~m0gp%5nWbyg@^9>< zN^uEdK$}mw_Lgu{&aBFcqi{AsInJ=7*oxYcfRo;o%hwURrmFUj^7dP@ih2N9y)UCo zr&1K42Qz&1{ZH;<4UZ@CT4=Njh@2okH}~uVhNujY<~pQqK1dP0ga0>@>KTBV{H6X9 zuA99BWC+Hu11)SQ>^%Ucn-nF`bQm|wg@nU2C3Y;XBd44*%k`Fiji~jzw>p>GE_nBE zV!Adim0ugxTMJ3h+F^aJ9ut!gdZKIN1nZtp?OAye6Fr&D;9|ZS{wVaS(XL+G*w%Gg z]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 RfAb^&nq?BPA4nPn^gr^n&`|&Y literal 0 HcmV?d00001 diff --git a/test/git01/c_v5.tar.gz b/test/git01/c_v5.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a6825814b41eef3d2068cc441e4ecfebfd778dd7 GIT binary patch literal 12861 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 literal 0 HcmV?d00001 diff --git a/test/git01/d.tar.gz b/test/git01/d.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..097b1f6339e11b144d8ec10f855f9801a6951ca5 GIT binary patch literal 15469 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 literal 0 HcmV?d00001 diff --git a/test/git01/d_v0.tar.gz b/test/git01/d_v0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c0a3a6e7cab5984d3766f3bcfaf6350a71c3732f GIT binary patch literal 17463 zcmV(_K-9kU+;U_q@-!r`uefyYx|o(QY>JuikD} z(|_YJr`M|0daYS+H{iLh>n(kbH7_5vxw$9^xzE^K$Bt~_wpWguciQ_O(qS%FdRBP3 zLU0cLw_42>@V{2A*Ju2n#!&q4_}nr2BDfSmI1~T%cB@`%>5X<9_+M|;XZk;tq4@8M z?&a_v$9eeQtm>%$?N)on|7nag|NDZQmmvk`xBrbQ{F(883S*rA10ICJzsNn1!L3%~ zc>Xu_4F8+8c5RMTFA|C4#z&n0hPm2quC|3)*L7}oj4rQ(6NZ;&ZDqA9s-0GCrM1#B zv=185*|?OE)_-3N-0%`&!1Me6Mx)W3+5c&bN&I(xt7kbECkE%|f3sb0%=kZzaRUEe zdSP?>LiQm4-#EelH?sV1)tfW_Ka~NDUt^QFFT=o@+y8$)()u6r-lY@)o}m8IZp`XG zQyGf?wtG3Or|~59pZ3iEPi3U}e_2Jq^Yg#np4I=SGsgIT2}Qssi2q^2jQ`UZ$MF9` zi-4!Z|GGX_|7+Fity%m(jZr=0VD08=r&h1)t#+-}XbQ90ZnyQi(W=$VTBp%4R#&?n zof{@IU8mOcXWr|q#ou!xRj<`n$B_D<1${Oqjc{VY{;Buw zUcdbfV^hBS15Z62K6QI;?(<)poBO_hl|OTH|NNW(_~k$Po4?z7`3uEAIQ;p)|I2Ux z%H3a^(@>7Ap5yu=hJ6gY*D~;)e)*fdzc=@k3F~)% z@8ehh>mL5q{^2VRX#K$_9*nMk>PP?Nt^Gg#i+}srdtdwRU;on2{n$4@_N}M?=MU#T z>2YJ9_r!81vTU1{WpH~ ztN*$1_1|j$)whbPb3Y!#_4a#~({(EY?pR%LQOBJ3;1l;o;XYmd;&=A{>4$#j-w*!m zwcmaBOP~JJtJ`hgnETXma6dP9Yx`gB|L?o6edBL`(!cp5#pqLi{@d&e7sY?2|68|j z-MX=}_rms#om+QbyeQymoD%Uaob_C4VkIC$9eV{+*Lia0>-g8#oR6{j9gL{Sm%h<6r=z^!OAU( zDhpl2M!ppaa}C~NY%zl4=0Wb6F9H!VOnlhYu4RjjN~My=MUexi>xm))%MD!9>RPZG z)*h#(F!6)t<&zAh|E35G-||AsbuNC7)AV20_00VrM3MEG{!e4P?l^oPOi%>R4Xn`h zkFGFbf@Xw$P&iZxR!9j)0d)gcknSdnDj!PWpN&}mFRS!J<23!R*4o+o-$rw$|5F)B z|4;hjXQs^`m7(+>qZ(nBQ4lVEk5m1By_N0%)$8q<{!e4fQ$S0Mryzcfc|zC_5S+c< zI_n<%5D%sQeb?Q;v<5KL@pJb7>9f!OO=pboKk`f-ib}u-o-Iyad)9FZ|J&7iHven3 zTGg5TpT?MfX1N3QI_Oi?WYb~d5pkcPL@=56Q4z&N5&n$nT= z{DPj>Ku3IW0IzF#4b()wuIJ;0cX?NYfL&i0`!DvZ9H3x7qUeFe&j~%kUL?CUKwtL`Wty3kVkDu zn<4zs#cZLhtJ%Tk3y&GX&2gc0H+fEa+@mIW*54-1KermN8TqGsTwM1CQ) zFb_(l<;P2w`6c!a%fk}I@};FE_NrW##zA_FJ`bcF5TxFt+|OOPqKSYTLm8msgh!qT z;0Xw$k}}j>j9n^~3jQg9tt)^o$2TXTEA18-DfDs`?X$vn*Yjs>w#iGePeghhUL%N< zaLc#$5Vdu=mnQ5$jJ`pvD;qp4)6vUejDL~jTv0gItWvxoEhb{;k`D$gmE_@o%DH*) zkL3{u2@iw5>xX->!CH7*y_%m-U_6Ft>S$SA$+KtRl%XGq)FE>zhVFn(%I;cX3nROh zkT3B5sBeI0DOr*HPD6ejs8B!|M;`~)hJ!Sf1GZZrIVQIoTRY?|2&Bh*7$_&Ol=S3) zsyURQ5xPnCNV+MpkQ7UkVyJ(o5@I{{h|(#aG4>NAEqjPDFLNGwyC{6) zYSAdYLm#q)B&?deV3IP8t-FY`W}v>WLoZ2uH=QF`10c~-Lli|f;66Bs^z&;tH)WhC zxgm$U2KH~?aSt7GNO%t}GaoccK&3?K;2oMVYR)BgL*z1^?x!us3i=Sc(uC@)bobF1 zBlRDtjO9VlyU-fYDfJ({*=}X(KdnY<*8ek=an|~ejO$a)LAKkA+j+*zK57mXd|XEi zi#{cSFtkzd8skz}9`<>Nk@J{a3myun8A<|BuNJ(0_x5#_86JTCU!e&LlJh@dIOb66o*esF)Wvb1r(SXo+F9y!JuyRR>Mg(86K0Sc`AFktxy3jjLL zu;9Vod51|)D4%U?&|{<(RcT^bg#?GHFlOZ0mVt~Or&a(Y#!nJJo=5@QBJEgskU^-R zHr8@1fW!jM5joaFRyN`jL8`F);2z8EF6sDt*(?+Z&$m_b30{_MpNvQBgCBnnIN1^V z?+s;On)bh5ZD#L(T8+l+{&ygh2L$uRqP&#fDW}7Qd_5+Ta)c(h9IxHQx z>gTlSSfQC<)sq&}b6Mv^8PGa|$B_3KafVwiE0R$2=td^Xm6-+aamxw9b)2HAGxO|* zV}%yCS^Uu$$Yi@r%}hFFLtlW8>|hBifz#-$)S9NzYSfHQOLSIwwN(|(MyI=CwpME^ zE8UI|jn!&H6)|sgN9q>j#fY{%Ff5Bsisufn3?>f`rreUm0oMXqQbpDRVGS)AEd{1( zfbj$>0dDzj5HQc?Vb}EsOx5pzCBh*O!Vg@_oP@^}bVLUr9~2OXfviP$1Toz-rT@v+ z)ZY*wIG*h)x7_7MD5DAK&p}9>kdEYIL$K^w28oih1CJX5+nOQQLlMJzH()7SAy{bH zCXgVVOvVSzmOqfOueg|ssjV5d7(j4bM&#sZWFeu#H0+d{E$*YupEPAjL!Bj0-qiu z02&G24?--@gAD*o7tRykQJ5kCKSd=ULJeG$bd{3dB%$T9!NJLb2P`AA%C2J{!E1aE zi;FT{czs5GfiVUQ^ zkwMR>kWk=Q$;(CYQVIkN&>Mpz$wAtH*@J*tqr!ja`uq8K4RO~KF4{TQVJ_Y7!~z?P z?Lbyeasd`t(g2zVRuHSo(0<4`kYN+~C=8{Fn7vCIivJ;dOp4g(3I)z!#xUVfLu_T} z9RYKDYZIcKNC5Y%eL4{ ze83%dsKEI|V)Da+5q6Zk1!M)=(8bzOUM5a*l8V16?4f)uU~jn{`7uF^fK&!B(t$!| z1fV)}8$VtU+)@ap&4zZ35-@O1* zFfLnR0x*8qXdN5$=-QK1+-Om1rWwan(pT; z>_zk=FHDd&MJNMW%EMEQ53~dIIiwJ{c5NB2>#khU@XyIxQ09OYz0_seD;VMn8QcqQ z$AXHPUy665f&$B2%*s4W8D;N$g|Qm}dbHaw%M=4A!vEJas`Cn2DwiOEC$_Eb%dcrf zFc2FeR{3Fa&Po}BH=3+p+-+e<_Zus3{|7v0TQb+JdNU@ z>{FAK5Gx#3ztG-$Q?!F`DSW)?U?1sn6Nz2x)dI={+)sxQ(BQf(Ys`~1WCE= zn_6xX=VA#`7weN)7h90XD~E2GuCGZwVy5Swh+#QtGL*V%c=N8hp(J8>&{KsoMZiQr zbDr>RD*FTY!I9DuAmPI1ofm)hUUm11dOiqxNZ6%#2Hs|=kE9O|oyZXF6dQxJ`%Yeg zS^RVg<01eYa1XUMms@_Yc&}Ps<>l_~b49j{^K*;fu+}%2Uc~<%v%s^haB=y*vs_Z> z>AUn6mXQ!gVT%D<9D>ZiT&ye=7bM`^JqdZCM(3LdyS7^_N;n`2i_gez9k!6d|83f^ zya7~ttftajq*eTXjw*EQ?#}iLckU1sB?7s~F|ns%_~5ZNHapMa4fI__LYf9Yg~aUT z(5tzb_|hqHyF35ZvDJEcxm{GbP0`|24+ObDD}4kF{lQ_7KJ6d zu`OR1A=q9JD9efegW%=3eTT-{02*M+IK*@59frtgvSA$N%Z44Z!r}t9Bc#sIH@T!7 zrEIh`TpzK(^@97hg{B%Lk?)J3ge63AIEInz)}mVqqzeT$1b|$s<;9oNF>)2v6|P!< zATqRw2#zo?`Sg#-E~yg?aA$lWzzg6PYPf7)2v2^4hKiP0V4+}0vb_Puk~~cMtYliT z_$!5B>5U>4&{JkH)X9}rY8e)QVzZ2P$-46h8bDwa8<4Fyg+S-%O5tL> z+XYES&Z&|$m9X)11;7j!wNG+nXhBkhBmjs?Oi-{WpurTRXp6Ozs7eIQhFnA4Aiz7A z-VCeQvMrC=C~hH%K-jT*?tspeG655}sx4%WEka{1IBCG@mnS%ru3-?7&? z3m7)sVz7)vC3c8QQ^ z7E@x7LAJ>`lt_q@WQ$56WG%}`*&92Ny;LM3goETa&x}gjd(QdP>G!_xr~AK|d7kIK zpKHIb`+HqOM}I6F&56+*qrit!=#Si6oc^b8r06+G`aS#$k7YPI8`1*r!4h)(hio)B zn}apIP7mkcxR4>DMJGfMLPXmY(LaL#7kb6Qd+Aw}`lpfD3I9QsF?c-b??7lp8B&R( zpD~i07ixMzk)b>0_j^Dm8oOu?00TYb;J`rDihoEcBSc|HdPaF6)E5<|#~h3#l8rbP zV+$3d%?qMpB?O=nw765kQUEF-As_*UpAwcIcqTy$$f2~a9xk6Jl+w^RS7WMB_mzJZ(+|56^9=DCYa{~I}fr8EN3i)wjd!PD2y&$A|g-) z1*qhrH9(=;g`ta*-v3QB@PFHc{+}M7`hN$~hn5XL@C5j;>Oa1M|3#yJo&Wxn@wfax zy$qIq+`C9X_~}^1MK0vCUl;COI1pxlI6i3y0y=*T4rlEMT7C?@4;2XQpr=q4V?00Z zh8X2;`1h`Zb_)r#D<5FR3_PQggUg}AbOVm3eO`b#g2+D5D1K4>Pwxr<8bL+c&+6cL z?fd;U?P?4LC51%5rSWhq(i$aejV4GFWMxn&H~}LiEsK)I<8gRt3_=nrEki(KumqGf z?RNx1MiMO}BTJA%U~!Ttq%{tYgk#aN1Q|H$E3NiG@{z1Tk8JPkWP@E$MLNwf3khUV z(C4{)QXN9y*w9@VjbOAVd=5dRM;43u0mc;K47@1mFwKEU;2m%hj7W|4;nT?%-za7c zVt0NS+5e~G6Z`)W@bp7#0RIaAhn7Trf&WANs{i;Y<44s0LBVXfJ#DAPK<52#ME`wK z&j&u@->K;PRDu2hYH-o!@7t)s#TWf3)Zmx#-;Zws_gDIlKudid|4E~M#eY9#{L}mo zBToCI`wl%+p_jY3(wp|{<9x_RAp{lZeWk*}S2R6xqD>r6&F9#OWfk(Q(RQz8a3;NHfH2s%40RNT#OG-+9@&Av~zyAOIr;N||4?6cB zE(Cvz|DX1MN1}iAfBq@sYy78ogh!yUNEvB_6cUHF#^YttvS>+ytQ3yEHzyDn>nJ|4b-P(Gf zZ_d>IfO&tQkgdYXX@IN@H1F$gH=ic$!;zC#q*N-0(^qtcN?Q>(Ffx6*^JHceanq8O z-hyp*#8O2~qx!qI(Iee{e?*$7p_sSYZtOoCu=~t0U(=3N26dGeohHKqOon&DbGl*&n6OAHr69 zuBaHetS2AQ@~qAx=y__9vxrJ3gGRdGijmz9N8nccs*^5*FFji0SZp=LIt(=W4jO;C#m z+iLatt^UBv6w+YKd}nxe#0;Q(Rpo3PeMxYx`;ri)28HXD!Fy!`R&C``wkQkQxGh2K zNYfu3CFQk~6IXlC#v&&xfc9HSZev#?q1rV$*2QwkSQlK_VavjS##9Y{%+Z{(i{~8E z&iHKd6?wUT4|^p4xpj&&4->}GdAYE7>k-oZ#^k7LQ-i8w85C z=Yn_-y%+D8wDGb@|I*mV%GlqTszLF7M?zmKs$LdYrK&<-QqUst4s*`x;`IMh#NE!W zRPpc!@y4_ZB@nH+)z}cT%whgIrDw&I!jyt%!^p0XBb-syx&ka^-G3ZZrv?a^EyZkL z&I$y?_r&X{pS%)8O^nMa${)?zkX7{V&!ST{JNq;uE(|2rwLPw4ktf5u`CQtQa)rG6 zlACYOF!SG6vHFrq1i45XW`0Y-7}&au)NJ7wuU;{z)DkTwv`6>6k^)JB3!bnnLS1pM zK{Ut7n6vI>Qgt;1{Zcr7^#-3aSGcr<-(0)1X$kb!J{N!z;8`G%LT;WJ8jiD$dqty? zm|llskKBR8KsEm5zEk%nS(%DA$USTsiz^VGAQnXjD_=jyMO#t`eMxFlvTBpcy9r@% zkZ-%U()hNU#)mCT#AkaYKB71 z>$k27GCUPyJ2G}CO^ZBuywJ#sr&z7;OSRKMo?Am>-Z}gcfyrG_KJO@9dB=|BIntPi z1C+KaR-i5)^|^4*ux<(L;H03(Ei7kJI65*)NN+3K5{2+;9?tcUO~ede0SAvz)$_$7 zitTsoX3Pdt*6*ak2M5zfIZ>Q0MadPeN?yB%@H8G}!UDQZtKk-5k#-_-LDZV)hod3PFl;U1y+)qe~=v}GF zk65Ec7E;r>y5}^EsS>t)+r>M#XEWP1QtO7_nN7CEdKcLrZJ(TOd2XF=x`O*rJO@QV z_Gs+}m*muexPrBV9bQZ+eac1jB?a!{x(}s^c%e_hZWfOH7fSi=GE)=yIxbxhn(_1; zA0`sQ22%XfE-pzv@mN^0uNpYn=Ie%H;|ZkVL&H3-8TX|HFuObFG_#ilN3-47>w2MV zwPL6LbzWaB&$|*S$FnClfYLNEUC}D0OOvLhmWhG6wLx3tRMYi^)?Epf%jtezci`cq zi4`+M_5F$%O6RUNtlV~on9H>CZf4DJyFc-o?k;Z!hi(dIiOvCIg(yB(KA$9>J!kNp zxTyTqGNXA^CZ&V`&(UPk#M))t)sK9!i2{U#QulfpRi4$zbBehy+|r+B&S(hN5#EZ< z4oU1RBJLp0dO3_C4v>}3oaZ)ry;Tz!zGakTJl`1;etVXgc}TT4l8t>a=vc>F>&i`Hf*bZfD%Po@M24~4izUpqr(@Xf2NTeo`bh7xvN`thZYv1oRfm_oLdm4e5l zH1gB{a;1!#+@pJ?+Qrccjp7MeiK1-?;fD3vb)r=| zbyNv0Yd7hsh9TWG>9U^3Y|}(S@{hjQt^Zn8#Xp&M>BzXm>>jsE=FBHgHvV~xx4LiU zVZ$LTz{BL3R#e;|42boD7z{88E?mSjhSAP`V3DzcQ-3nm^HHrM7IXQe zI1v2`0$+4d{ef)!Y0n#D6<3@TC6+KUlvc50%N~R!@u`MU%QmNB2mXFA)6@I+*P+;c3$~3iX17^ z+yXC@7v`-B?^e8x9*_&1n{QjwjVu*k3N+u7B7N_hz8=160Ndv=+u40}P;_$skd<2< zcL5*xrq01)Oq_G}p|iRp56QKbhD{xD$ZrOIU!@Z#nWm>RVU>Wj5Z`du}S$h65%fY3!9gD?h~p zgg=8kUZlB|CU2jr4@{-Z_JmL5iMM5;Cxc>WhN1{^>*Gb4rEiTMgl0`;!SZ@KXGO~3 zF1cfd-axD!War0=_*-*F67yQHd4q*^=(p{|td<|Rrb~;$;N5^g#AnrP+*!S|-fLu7! zyXV_ly#5R#iy}D{`YAaQ@nyRBMCXV^4`1q+1H~OYKozys# zSXDkBM9TDx)~efUCO)?UD13kl|Hhn}akq5phF&&2!)B>Yv_++7Z`k8{9RHQ~e4JwO zq#@EKbJY83^w6g3jY#7Q+Vf8h_E$U}vKBLhV|r`DROUIxR!vFh7B%7RqG|Va$HP*V*zR$B>wo`=P#4Da5lNV@m&4vSDKN94booAsK)I3bntMfrT0^I3XIE6lg@t?%_Ti{EC{cl=UK^h?DJM+W&i(KjtU3g4zQtP7Q& z;(f$rYqvLF1OjlpMx=ks@=e-K_iE^oUe;b!9}f@o?tC3;L-E_9YPQSOgJ5Z1WNDAJ zB$yvLy1c};F0p@p9ZtndLWI})L8pFJq*2!?P+VsH9%_d~cuScJIQ7RecVL2ow2qx* zVr4;%d}GdAR$HzqlA2;#u0?3&A`8V0EzE7W4}HzTw`UDZ-z|T{-9u$pi$qK{+4b$6 zdIE||5oV(C>2s@~7XQw?>&337xGRkZFo5k|q+cTQ-#2~0x>qapB;BG}z%&gLf~MzB zm;s;}aLE77E!yR*zU_S_?y@@hIP4kEdgaVFqO#5AT9;AWP3JVDyI}czDw_MaRf-By zZh99_B1YgPZ{3i|+iz?Nif3sVu6NLWHvWvfTCi78=4D?a?Jllf#Ob#|WU9eZSA5xPah zMLf{%pr}ogy}pyZHOErC$UEmTSYRXLv zPIOrl_kFzLa0TKU$N_u7qA)#^b_ktu)=b-t922(5Gr2rCI?m4IY>-=+poks)h<0h1 zS;QvB{;(6Pl2eH1-J~EfVM}j=nU+g7*lU1Yu16>Qy2}t)6061@7+7EJ)XuO` z`l@4nAtJG?diG1d?cqLq?Vjq`QHa2PmYyy*MS@v_R8=(obA0NRDE=E-H@I3Gu_Z)XwBxf?M_OrcPM3a_T%*tm1^;s zH*doZ$fm~}BJWvGS-W)Skrmcjxt_c|wqYt_>BiUL3b#!VTsRGmmBM}UvXx2-MFUkF z%8z}m3-w8HImzApeAg?lv~~}Q*!q_;?w`##ws*Jn=YhlQM+QBV zj5aCKaLC-x==R59Z?z?C%PLm)Ktne8WbUYAB+|l!PF8HNM{L-zFQ#S9igL0C6r#(r z*slvJJ3T|Ca&31)evPPDs-hJXPN*X0hVy7;8=d)F_vu ztp_Rr*3ba5z#86Db!r=6orF-aV2Cjbdr}W{*>9QkGc$$RQ@+dWotdf3p88#8`^-!t zd)jxIqcby&*$;h}Su-<}%$`0~;d?-ZZ-uy)%1`}5%BSuRYe$GRYeRTF)1+n3k+oa} zqLL-;OKs$X3j+P`Z(Y`xT~bvsI53VMDBE;84$Z-R?@fq?rCz#$%@E=7CIf`X7%^6` zDJJ_MS>C3>BKcABE!UoN4Pu#Row5gZdc_RGdG5U_kLOwCu1GT*<{{?-vzv^l0-_jSsdyYNxHEBe(~9pD7g8 znpl1c=9~~V*|YH)HX!LW^u^^{XKY#AD&G$8RC_gKk)ZmLYxCQ67x)&o`TM_K|9St9 zJ=Nz2+xX-DA2iWQ>;R76 zfpZp9V?nrQz%vWp{{X&OESUxW3V8!pC2vGV1lz+g=8L0dYs(@9~sQm=$Gl2h_AOJ`J3V;So4L`vC#o+%9 z2-B>d!5YBYX*2+uGgvnRZGa9y_eZd9Tb+H1TWE3O62{{!ZoJ*;Odi~lT4+(_@#;+R z+_u{s7ROyNjGfZ_4Hk|aTdKOej#}uj7)nY!4Z2Sh{ABZLZOn%^-rQErsJr?4>g^Bq z2|rU}NXHi}lKLv~i(l^sm8M)gizxkGcM8{tQOx_j@1+h*oN3i{RbNJIT+0(-Min?7 zVsxPqE3A9WoFWxj){a|wQtA=!;5L~4xh)=-ApIUPxxR24olR>* zeUekIcvMchx{%6`Q)LI+^ok2}>%2c9&j>wvCcU^y>AdVy*rsBZ1f0?B-j#Aun1{o9 zG6SUs>;_G}iBz7ja8sBdeaU6l46d>0L?byL{TO@n`N`1CJ@ML}cl-#slSVITa#mgU ziE)Q+mR&+Dck-zJa==9SHs6jQ3B`%Dp8Cd=#>X}&$RWp%leYQ3IP zCb#_J!;YNyYjuw7G=EpRLq1FRT}XG?x#v-kTgOfDtF|FSxAA%3x*)?aZy%^0^ION; zl46Nt==3}ky>&@cBf=rt0A&yy=xQ}S^x5Ha&>^DYGeaeb$+lAK-PU9)qt#s62&wUN zZ!M|4V&utC(PLDVYtL1Zf=A*HW{V~-@)-%5$ZE0UC^ZR5+OWMG^DOAYo`QgcBs$bC zIZqAyu!iD}B`IcBLE3a3=JEFs(x{RDs2Al*y7 zPcu@9z9n9s8FC0a+4XO?dBbXDTS^Vn$Hg1{>-@hKCyU^<0?x4KITKf_7ZdsP?wN^=k2b|b`>faO9BOtfQ0lCbeE!5SyF2z!%;hAh7am#%OGp=JhJ6xS zTJ@sBl6&{`@a6Y=T`?^PKdf+xsEc!>9EPAD)t4ikG1*k{d#{OshuC`LGw^tUGRB*# z2I(Lo{UhBn&dLybd>EZtsC$w#aJwxO9T=KW6tw{RYdEm7SgLm`BXhr%vPlLwK^;tuUsqg z{=CFsVbXwC;`*HHH>}@ih*cHeuL^ll7kt|LNW;sEfoCTtB6X@A^S#GcDkcq`Okbn% z)Y03}(VVR&Ofw1zS-?)WV!uTfSKG=MiKoDr6XdBo#iUD@{+gyks2)pG1wJ`@{@ zczouNnrrmwkYcr3m)lYXkk6E>xmL?^fqUG!vEq!)}pT1_lV7G`oz)^mMuf4O?aYT+HI$A%vJ)Yp~1yNouDcS^oc|t$=mgTTOzma7c=a>a?U?R-# z*rUQj$NWXF+7$VDxbA5iXYKF<7A_570B{ACs|?siaLwfezJfO$Sgr*3S*5`Nx3m z1U`*#8(3dHm`<>8w-xq7SC8g6&y>Jm9Bb zdf?c$g74s3%nN+5U@Ay+`B%5Xqqq5V_TI1$5nqm=o|7r#OY0Gt+Sk8Tv+n=s*uP;2 z@VD`wxIa7o7c({fb1I2B{&OBQxBT%KESyZBVNoO`j*6qh5pXmCPD2u?C@LCH$AWu* zG#&L*r{M?p|5Hs0-< zy}UWEFl#gJVtc-DnJbfzG4>OUU!kBI*$9D+Iie_k}V{4sPg6^})r2m}(2M55u~ z7!r!_y*H5xLFWUhfDQxMkTcm7y=EWgj(FXs?y#C;e2gQTE_wm_Ax38CA z+vSd~HP+873~_O1D0Sdt1@-#mod|I2E{UN+SI$K5(30x+%Z@b<#BqtLoa^XfDK-l( z@$|e%<@wn0!S4BdUbpr&RErtbIuOHz);>{#d{XKG5b>OTVa&xrq6=JOxsM{~;`1bYIV zjK)&PR16kF$5An8ED22@lPM@Vl1{~;2^1QI@-y4`iT0oA_>VxLna_Wi7yVuBKg&{1 z)oqq~O1-}o-S|2n&@__j!r+F;@TfE`^bs>PQ?u$ng}UJF|JYyKf0)mInitJ2e*z9e zz|p8E1cF2*Q|KfV7J&v|sYpD5PJ@%NNIVvg{Y&FN5{3G{{E-MGj`{wVd65q^H*pPL zl{7!w7I&^Oec^_>Vl$zhm3u7T2(1g>oAS6W#ckh#53#dJR) z`RiflwmX0hx^5o0f4^AZ#}9%mAqFzg=O;3=DUN3w z9`s$!(sVgpczt5`SPLbevD`C@tm4gCE8Y{|s#L_4Ciu!PlJBdIjD7&*lFi19g+4X^ z6`EE5vGjjP`#*F1&-`d^`6DPu+Eg4S5D-*21(XX0hsTjgcoH6mLR0A|8WKrG{iW^y zNE~`r`#&6qV)p;ei+rH8M^uKOUi`QdJnO5^DhWM_j_Wntou6cw&fZq; zJ$7@w0;{u|vGm5~Ba4QkUc8i{%54gV6?Dh6+UuAOTE=&;K#v~YO=;jh!=k0|`HZE) z-OV0qCiOYJj8zY;E}8JIFbjC#*rF1xqSLb*?Z|I_?n%9Fcym{=d9XCKUg7oDTrPk4 z2>GbXw#PYb&gQ-IRov#$()X4790}xXLnjbuBqWlCB9rkb0u_&ilgM-ennXfj zFhBKR@ITf5^L_s>n)&{Z`O)9i{&S=uu#KDQaUqMCzp}RQ`hy_TtrT|1kgqc5LLV_x zGc~LJPrX)QL7)Hp>+}E7XcW``&x>Z2Ka%hdx&AZ9|IUx*mOlxHroeGHIJgg_BFShZ z`hVKH8mK6)D@-kk*-5I=ia%A)dAp$e$o|ai&i){4BB+QpK~Xs|1kLQs+ht^SX3y-b zfXMovHfaQEqd7Kk8VOi`nkdG4Obsd~*67iyt(qE*^{AvV{zplIrk=jpT@{x^!a1;; z*7-QVoA>T}clOxvz^6N9$a!k507J$S(NS|+AI%AI312ondXk{ zKeGGb`~QTIQ0#vsAm{(uE9Ga;O+P!6%6RnYS@F*eoYZr>Q9T{x6>MWo+0tnXBC`4&A$I-qGg4ALlk- z*>EW4hN1Q7xTcvGdtdF9E%mutFrok0p-ZTXi-VH^xqQw{r}+oe@gyWWYAvz z7Tjd8LTDnLHfSSlw3VT7r*OzE-uWm!nf`T2+>!+Yt{wU_b>Vg$?e1MxwryH$qWWslj>@XnYLeCVx;J)x zGg&iabVJXIyun!Mp3H{zht6f39k{)E=U;z*MDv@MKR&~Z73|Yq`2DDVWxVC<*}^Pb zeqg1(bQs(>iCnZIXW_cI0-)KSwCLc4oW<1*+p6APbLNz5!^q`HL#p~MtKPMxet5~v zzyI#~-cjWZk3VnwqG|W_AQ8^Kgp=a ze~4iGN6G(+2-?fvV#7(AG#W?}$00>AIHYM3LW|90CQT&eAQ*#%!8^mFL;G(e?w3EZ z|3>Bc9}z)#{#WX}{I{D^WQrBW&NH7E!Go%she@-g_LI3;nD*eG>_p7-^f6=|D${LP(K-Kzm8oVKKRjrRUZ!AK|HZ>PRxE?+10Nd zXL_gS73Y3>vZ%b*+LmV1mg-?8E-C47T7_z8|34f`+oT?69{c(^_v!tqGd4Gy6W;7S z|J4Bu1oQiG>K;sO?7O?G%QyDY<{rszja^uA?ejM#Hojf?-u2a6_Fbr4q5rga-L1pN zmXtMS8)m1d3#(jluk|(7WMu`@e>0|!Z;kfMJRAt=f4A)W-$Cd1{|P1j6Ct#fKP;g+ zAJg^UsbvA19~A#Lx6A)9l1ltPB8Ue_{94Ydb+R1v0~;JEWZ;5ea}4C5 zn35|5;Pb*jeR3M=oTAGYr~*^*R9qYgG{`Y_2MuJfEKO)Gd z1rZitDiL}FFDnURsZ$W$lr$R|0aoCF9T+f`Y#ZDHQa?M8L?6Ud4q3zPz)5jlbi@fR zisMA+^m!OcLN*~pmM@}S5(FASWL+$;L2R5*q6<;`2t@7G7eb2h>QmAJ2SH-5K1i%j z!BjqxL!>S!ipa1aqd}yhc1h1*`3{ld(Ks46D?12w$5d`8y5!?FjDpmP z322L;{qK_8>v(je|8i{TuKZ21{*xAyS<(MUV2a=qX$aB)gXM)T`DwtQ zLqFIv6w6EK7mBD#qTnna4+^RDU`baJ0>35FW5rR<|HYvDh1LvLLV>w34Ko8^@$kU~$(t*BQ zOB5PL_A|7C_r8{B%cwi?{rd0kG9Df1 zKW-q&uF+=yKe%{|;IDx`52Pm`8{HA3=us|0at`iT^|dGzTd@DH$unRH7SbMJGs@ zlAAduJ6E5go5Om(qZUn<&NT-s$FO)1STk7QZ&EUl2SJ{fUGpMMNg$ytHz#K*>Qk<# zUesVJ)(P@~rX>Xn@*|ij)QvfRS>Xmd8{oSi(vGmxE2gu~{BThU2i%|Iqju+0V{I zW7_YW&z;rF=t3o3LfO>OxmjbU&dixou4gET(qk%j5yOf=>j4R&{+NpP0PVex#oTSt zBks`_N(8~^nQrINar}n}^Z#(8lK&eSC>M1F3KS?%pg@5F1qu`>P@v%dAO8hSiE6O` GKmh -d [-u ] [-l ] - pkg-svr add-dist -i -d [-u ] [-c] - pkg-svr remove -i - pkg-svr register -i -d -p -s [-g] [-t] - pkg-svr remove-pkg -i -d -p - pkg-svr gen-snapshot -i -d [-n ] [-b ] [-p ] - pkg-svr sync -i -d [-f] - pkg-svr spkg-path -i -d -s - pkg-svr list [-i ] - -i, --id package server id - -d, --dist package server distribution - -u, --url remote server address - -o, --os target operating system - -p - --bpackage binary package file path list - -s - --spackage source package file path - -g, --generate snapshot is generate - -n, --sname snapshot name - -b base snapshot name - --bsnapshot - -l, --location server location - -f, --force force update pkg file - -t, --test upload for test - -c, --clone clone mode - -h, --help display this information +Package-server administer service command-line tool. + +Usage: pkg-svr [OPTS] or pkg-svr -h + +Subcommands: +create Create a package-server. +add-dist Add a distribution to package-server. +register Register a package in package-server. +remove Remove a package-server. +remove-dist Remove a distribution to package-server. +remove-snapshot Remove a snapshot in package-server. +gen-snapshot Generate a snapshot in package-server. +sync Synchronize the package-server from parent package server. +start Start the package-server. +stop Stop the package-server. +clean Delete unneeded package files in package-server. +list Show all pack + +Subcommand usage: +pkg-svr create -n -d [-u ] [-l ] +pkg-svr add-dist -n -d [-u ] [--clone] +pkg-svr add-os -n -d -o +pkg-svr register -n -d -P [--gen] [--test] +pkg-svr link -n -d --origin-pkg-name --origin-pkg-os --link-os-list +pkg-svr remove -n +pkg-svr remove-dist -n -d +pkg-svr remove-pkg -n -d -P [-o ] +pkg-svr remove-snapshot -n -d -s +pkg-svr gen-snapshot -n -d -s [-b ] +pkg-svr sync -n -d [--force] +pkg-svr clean -n -d [-s ] +pkg-svr start -n -p +pkg-svr stop -n -p +pkg-svr list [-n ] + +Options: +-n, --name package server name +-d, --dist package server distribution +-u, --url remote server url: http://127.0.0.1/dibs/unstable +-o, --os target operating system +-P, --pkgs package file path list +-s, --snapshot a snapshot name or snapshot list +-b, --base base snapshot name +-l, --loc server location +-p, --port port number + --clone clone mode + --force force update pkg file + --test upload for test + --gen generate snapshot + --origin-pkg-name + origin package name + --origin-pkg-os + origin package os + --link-os-list + target os list to link origin file +-h, --help display help +-v, --version display version diff --git a/test/packageserver02.testcase b/test/packageserver02.testcase index dd533bd..408ca26 100644 --- a/test/packageserver02.testcase +++ b/test/packageserver02.testcase @@ -1,6 +1,7 @@ #PRE-EXEC +../pkg-svr remove -n temp_local --force #EXEC -../pkg-svr create -i temp_local -d unstable +../pkg-svr create -n temp_local -d unstable #POST-EXEC #EXPECT package server [temp_local] created successfully diff --git a/test/packageserver03.testcase b/test/packageserver03.testcase index e858351..11eb774 100644 --- a/test/packageserver03.testcase +++ b/test/packageserver03.testcase @@ -1,6 +1,8 @@ #PRE-EXEC +../pkg-svr remove -n temp_remote --force #EXEC -../pkg-svr create -i temp_remote -d unstable -u http://172.21.111.177/tmppkgsvr/tmp +../pkg-svr create -n temp_remote -d unstable -u http://172.21.111.177/tmppkgsvr/tmp #POST-EXEC #EXPECT +snapshot is generated : package server [temp_remote] created successfully diff --git a/test/packageserver04.testcase b/test/packageserver04.testcase index f2539ca..3be6257 100644 --- a/test/packageserver04.testcase +++ b/test/packageserver04.testcase @@ -1,6 +1,7 @@ #PRE-EXEC +../pkg-svr remove -n temp_remote_dup --force #EXEC -../pkg-svr create -i temp_remote_dup -d unstable -u temp_remote/unstable +../pkg-svr create -n temp_remote_dup -d unstable -u temp_remote/unstable #POST-EXEC #EXPECT package server [temp_remote_dup] created successfully diff --git a/test/packageserver05.testcase b/test/packageserver05.testcase index a66a040..da0ad18 100644 --- a/test/packageserver05.testcase +++ b/test/packageserver05.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr add-dist -i temp_local -d stable +../pkg-svr add-dist -n temp_local -d stable #POST-EXEC #EXPECT distribution [stable] added successfully diff --git a/test/packageserver06.testcase b/test/packageserver06.testcase index 9f9e917..cf49f46 100644 --- a/test/packageserver06.testcase +++ b/test/packageserver06.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr sync -i temp_remote -d unstable +../pkg-svr sync -n temp_remote -d unstable #POST-EXEC #EXPECT -package server [temp_remote]'s distribution [unstable] has the synchronization. +package server [temp_remote]'s distribution [unstable] has been synchronized. diff --git a/test/packageserver07.testcase b/test/packageserver07.testcase index 1b7d84c..13bec0c 100644 --- a/test/packageserver07.testcase +++ b/test/packageserver07.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr sync -i temp_remote_dup -d unstable -f +../pkg-svr sync -n temp_remote_dup -d unstable --force #POST-EXEC #EXPECT -package server [temp_remote_dup]'s distribution [unstable] has the synchronization. +package server [temp_remote_dup]'s distribution [unstable] has been synchronized. diff --git a/test/packageserver08.testcase b/test/packageserver08.testcase index 9ecc71a..db9a935 100644 --- a/test/packageserver08.testcase +++ b/test/packageserver08.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr gen-snapshot -i temp_remote +../pkg-svr gen-snapshot -n temp_remote -s snap01 #POST-EXEC #EXPECT snapshot is generated : diff --git a/test/packageserver09.testcase b/test/packageserver09.testcase index 547b101..d8632bf 100644 --- a/test/packageserver09.testcase +++ b/test/packageserver09.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr gen-snapshot -i temp_remote -d unstable +../pkg-svr gen-snapshot -n temp_remote -d unstable -s snap02 #POST-EXEC #EXPECT snapshot is generated : diff --git a/test/packageserver10.testcase b/test/packageserver10.testcase index 34ca9b6..7a5fcb2 100644 --- a/test/packageserver10.testcase +++ b/test/packageserver10.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr gen-snapshot -i temp_remote -d unstable -n test +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test #POST-EXEC #EXPECT snapshot is generated : diff --git a/test/packageserver11.testcase b/test/packageserver11.testcase index a7ba031..247141f 100644 --- a/test/packageserver11.testcase +++ b/test/packageserver11.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr gen-snapshot -i temp_remote -d unstable -n test2 -b test +../pkg-svr gen-snapshot -n temp_remote -d unstable -s snap03 -b snap01 #POST-EXEC #EXPECT snapshot is generated : diff --git a/test/packageserver12.testcase b/test/packageserver12.testcase index e5db467..092eb4e 100644 --- a/test/packageserver12.testcase +++ b/test/packageserver12.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr gen-snapshot -i temp_remote -d unstable -o all -p test_server_pkg_file/smart-build-interface_1.20.1_linux.zip -n test3 +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test3 #POST-EXEC #EXPECT snapshot is generated : diff --git a/test/packageserver13.testcase b/test/packageserver13.testcase index 017edb2..ae8c629 100644 --- a/test/packageserver13.testcase +++ b/test/packageserver13.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr create -i temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/test +../pkg-svr create -n temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/snap01 #POST-EXEC #EXPECT package server [temp_remote_snap] created successfully diff --git a/test/packageserver14.testcase b/test/packageserver14.testcase index 62d6676..06bdd06 100644 --- a/test/packageserver14.testcase +++ b/test/packageserver14.testcase @@ -1,7 +1,7 @@ #PRE-EXEC cp test_server_pkg_file/smart-build-interface* ./ #EXEC -../pkg-svr register -i temp_remote -d unstable -p smart-build-interface_1.20.1_linux.zip -s smart-build-interface_1.20.1.tar.gz -g +../pkg-svr register -n temp_remote -d unstable -P smart-build-interface_1.20.1_linux.zip --gen #POST-EXEC #EXPECT package registed successfully diff --git a/test/packageserver15.testcase b/test/packageserver15.testcase index 351d6aa..af34b96 100644 --- a/test/packageserver15.testcase +++ b/test/packageserver15.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr register -i temp_remote -d unstable -p smart-build-interface_1.20.1_linux.zip -s smart-build-interface_1.20.1.tar.gz -g +../pkg-svr register -n temp_remote -d unstable -P smart-build-interface_1.20.1_linux.zip --gen #POST-EXEC #EXPECT existing package's version is higher than register package diff --git a/test/packageserver16.testcase b/test/packageserver16.testcase index a70d250..4d09776 100644 --- a/test/packageserver16.testcase +++ b/test/packageserver16.testcase @@ -1,7 +1,7 @@ #PRE-EXEC cp test_server_pkg_file/smart-build-interface* ./ #EXEC -../pkg-svr register -i temp_remote_dup -d unstable -p ./temp_remote/unstable/binary/smart-build-interface_1.20.1_linux.zip -s ./temp_remote/unstable/source/smart-build-interface_1.20.1.tar.gz -g -t +../pkg-svr register -n temp_remote_dup -d unstable -P ./temp_remote/unstable/binary/smart-build-interface_1.20.1_linux.zip --gen --test #POST-EXEC #EXPECT package registed successfully diff --git a/test/packageserver17.testcase b/test/packageserver17.testcase index 75ca498..ad1549b 100644 --- a/test/packageserver17.testcase +++ b/test/packageserver17.testcase @@ -1,7 +1,7 @@ #PRE-EXEC cp test_server_pkg_file/smart-build-interface* ./ #EXEC -../pkg-svr remove-pkg -i temp_local -d unstable -p smart-build-interface +../pkg-svr remove-pkg -n temp_local -d unstable -P smart-build-interface #POST-EXEC #EXPECT package removed successfully diff --git a/test/packageserver19.testcase b/test/packageserver19.testcase index d144662..cd5b36b 100644 --- a/test/packageserver19.testcase +++ b/test/packageserver19.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr list -i temp_local +../pkg-svr list -n temp_local #POST-EXEC rm smart-build-interface_1.20.1* #EXPECT diff --git a/test/packageserver20.testcase b/test/packageserver20.testcase index 4747a48..7d47e52 100644 --- a/test/packageserver20.testcase +++ b/test/packageserver20.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr remove -i temp_local -f +../pkg-svr remove -n temp_local --force #POST-EXEC YES #EXPECT diff --git a/test/packageserver21.testcase b/test/packageserver21.testcase index 2fc8eb9..1ae0b53 100644 --- a/test/packageserver21.testcase +++ b/test/packageserver21.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr remove -i temp_remote -f +../pkg-svr remove -n temp_remote --force #POST-EXEC YES #EXPECT diff --git a/test/packageserver22.testcase b/test/packageserver22.testcase index f63decb..3dad192 100644 --- a/test/packageserver22.testcase +++ b/test/packageserver22.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr remove -i temp_remote_dup -f +../pkg-svr remove -n temp_remote_dup --force #POST-EXEC YES #EXPECT diff --git a/test/packageserver23.testcase b/test/packageserver23.testcase index c92b0b9..6e37f2f 100644 --- a/test/packageserver23.testcase +++ b/test/packageserver23.testcase @@ -1,6 +1,6 @@ #PRE-EXEC #EXEC -../pkg-svr remove -i temp_remote_snap -f +../pkg-svr remove -n temp_remote_snap --force #POST-EXEC YES #EXPECT diff --git a/test/packageserver24.testcase b/test/packageserver24.testcase new file mode 100644 index 0000000..2657bab --- /dev/null +++ b/test/packageserver24.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +#EXEC +../pkg-svr add-os -n temp_local -d unstable -o ubuntu-32 +#POST-EXEC +../pkg-svr add-os -n temp_local -d unstable -o windows-32 +#EXPECT +snapshot is generated : +package server add os [ubuntu-32] successfully diff --git a/test/packageserver25.testcase b/test/packageserver25.testcase new file mode 100644 index 0000000..e399ad8 --- /dev/null +++ b/test/packageserver25.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr link -n temp_local -d unstable --origin-pkg-name smart-build-interface --origin-pkg-os ubuntu-10.04-32 --link-os-list windows-7-32 +#POST-EXEC +#EXPECT +package linked successfully diff --git a/test/pkg-cli-download.testcase b/test/pkg-cli-download.testcase index e4f6b2c..f4e41d1 100644 --- a/test/pkg-cli-download.testcase +++ b/test/pkg-cli-download.testcase @@ -7,4 +7,4 @@ ls pkgcli01 #POST-EXEC rm -rf pkgcli01 #EXPECT -base-ide-product_0.20.8_linux.zip +base-ide-product_1.0.2_linux.zip diff --git a/test/pkg-cli-listrpkg.testcase b/test/pkg-cli-listrpkg.testcase index 6a74ae7..84bf751 100644 --- a/test/pkg-cli-listrpkg.testcase +++ b/test/pkg-cli-listrpkg.testcase @@ -3,4 +3,4 @@ ../pkg-cli list-rpkg -u http://172.21.111.132/testserver3/unstable #POST-EXEC #EXPECT -base-ide-product (0.20.8) +base-ide-product (1.0.2) diff --git a/test/pkg-cli-showrpkg.testcase b/test/pkg-cli-showrpkg.testcase index 443aa3e..fc5cf62 100644 --- a/test/pkg-cli-showrpkg.testcase +++ b/test/pkg-cli-showrpkg.testcase @@ -4,5 +4,5 @@ #POST-EXEC #EXPECT Package : base-ide-product -Version : 0.20.8 +Version : 1.0.2 OS : linux diff --git a/test/pkg-list b/test/pkg-list index cf64628..74d4baa 100644 --- a/test/pkg-list +++ b/test/pkg-list @@ -1,24 +1,23 @@ -Package : A +Source : Origin Version : 0.1.0 +Maintainer : taejun.ha + +Package : A OS : linux +C-test : test Build-host-os :linux | windows | darwin -Maintainer : taejun.ha Path : binary/A_0.1.0_linux.zip +C-commic : ask Origin : remote -SHA256 : 52b400554f2a29dec46144af649181cf287c000b4feb65de72055ed9f11924a9 +C-origin : kkk Package: B -Version : 0.2.0 OS : linux Build-host-os :linux | windows | darwin -Maintainer : taejun.ha Install-dependency : C, D, E Build-dependency : F (>= 1.0.0.20101221), E (>= 1.0.0.20101221) Source-dependency : D, scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) Path : -Source : Origin -From-server? : true SHA256 : your_checksum Description : this is my first -project -descriotion +C-kim : oks diff --git a/test/pkg-list-local b/test/pkg-list-local new file mode 100644 index 0000000..ce331b6 --- /dev/null +++ b/test/pkg-list-local @@ -0,0 +1,17 @@ +Include : pkg-list + +Package : A +OS : windows +Build-host-os :linux | windows | darwin +Path : binary/A_0.1.0_linux.zip +Origin : remote + +Package: B +OS : windows +Build-host-os :linux | windows | darwin +Install-dependency : C, D, E +Build-dependency : F (>= 1.0.0.20101221), E (>= 1.0.0.20101221) +Source-dependency : D, scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) +Path : +SHA256 : your_checksum +Description : this is my first diff --git a/test/pkgsvr.init b/test/pkgsvr.init new file mode 100644 index 0000000..96b4f3f --- /dev/null +++ b/test/pkgsvr.init @@ -0,0 +1,7 @@ +#!/bin/sh +rm -rf ~/.build_tools/pkg_server/pkgsvr01 +rm -rf `pwd`/pkgsvr01 +ruby -d ../pkg-svr create -n pkgsvr01 -d unstable +ruby -d ../pkg-svr add-os -n pkgsvr01 -d unstable -o ubuntu-32 +ruby -d ../pkg-svr add-os -n pkgsvr01 -d unstable -o windows-32 +ruby -d ../pkg-svr start -n pkgsvr01 -p 3333 diff --git a/test/pkgsvr2.init b/test/pkgsvr2.init new file mode 100644 index 0000000..e97681c --- /dev/null +++ b/test/pkgsvr2.init @@ -0,0 +1,7 @@ +#!/bin/sh +rm -rf ~/.build_tools/pkg_server/pkgsvr02 +rm -rf `pwd`/pkgsvr02 +ruby -d ../pkg-svr create -n pkgsvr02 -d unstable +ruby -d ../pkg-svr add-os -n pkgsvr02 -d unstable -o ubuntu-32 +ruby -d ../pkg-svr add-os -n pkgsvr02 -d unstable -o windows-32 +ruby -d ../pkg-svr start -n pkgsvr02 -p 4444 diff --git a/test/regression.rb b/test/regression.rb index 7085799..177cf4d 100755 --- a/test/regression.rb +++ b/test/regression.rb @@ -22,15 +22,13 @@ class TestCase end def is_succeeded?(results) + i = 0 @expected_results.each do |e| found = false - results.each do |r| - if r.include? e then - found = true - break - end + if not results[i].include? e then + return false end - if not found then return false end + i += 1 end return true @@ -88,12 +86,19 @@ def execute( file_name ) cmd = cmd[0..-2] fork_p = true end + # get result if not fork_p then IO.popen("#{cmd} 2>&1") { |io| + # io.each do |line| + # puts "---> #{line}" + # end } else IO.popen("#{cmd} 2>&1 &") { |io| + # io.each do |line| + # puts "---> #{line}" + # end } end #`#{cmd}` @@ -102,12 +107,36 @@ def execute( file_name ) # exec results = [] tcase.exec_cmds.each do |cmd| + fork_p = false + hidden_p = false + if cmd[-1,1] == "&" then + cmd = cmd[0..-2] + fork_p = true + end + if cmd[0,1] == ">" then + cmd = cmd[1..-1] + hidden_p = true + end # get result - IO.popen("#{cmd} 2>&1") { |io| - io.each do |line| - results.push line.strip - end - } + if fork_p then + IO.popen("#{cmd} 2>&1 &") { |io| + io.each do |line| + if not hidden_p then + results.push line.strip + #puts "---> #{line}" + end + end + } + else + IO.popen("#{cmd} 2>&1") { |io| + io.each do |line| + if not hidden_p then + results.push line.strip + #puts "---> #{line}" + end + end + } + end end # check expected result diff --git a/test/test_bserver2c.rb b/test/test_bserver2c.rb deleted file mode 100755 index 0d59deb..0000000 --- a/test/test_bserver2c.rb +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/ruby - -require File.dirname(__FILE__) + "/../src/build_server/BuildServerController" - -BuildServerController.build_git("mbs_server","gerrithost:/slp/sdk/public/native/toolchain/smart-build-interface","origin/unstable","linux", nil) - -#BuildServerController.build_local("temp","/home/bluleo78/git/sbi-slp-public-plugin/toolchains/public/gdb_build","linux") -=begin -#case ARGV[0] -# when "create" then -# pkg_server.create "temp", "unstable", "http://172.21.111.132/pkgserver/", "unstable" -# when "register" then -# #pkg_server.register "/home/taejun/project/sdk-build/test/smart-build-interface_0.19.1_linux.zip", "unstable", "-g" -# pkg_server.register "/home/taejun/project/sdk-build/test/smart-build-interface_0.19.1_linux.zip", "unstable", "" -# when "snapshot" then - pkg_server.snapshot_generate "", "unstable", "", "", "" - when "sync" then - # pkg_server.sync "unstable", "force" - pkg_server.sync "unstable", "" - when "add_distribution" then - pkg_server.add_distribution "test_stable", "stable" - else - puts "First input error : #{ARGV[0]}" -end -=end diff --git a/test/test_bserver3c.rb b/test/test_bserver3c.rb deleted file mode 100755 index 28b1564..0000000 --- a/test/test_bserver3c.rb +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/ruby - -require File.dirname(__FILE__) + "/../src/common/utils" -require File.dirname(__FILE__) + "/../src/build_server/BuildServerController" -$SERVER_CONFIG_ROOT = Utils::HOME + "/.tizen_build_server" - -#BuildServerController.build_git("temp","gerrithost:/slp/sdk/public/native/toolchain/smart-build-interface","origin/unstable","linux") -sleep 5 - -BuildServerController.build_local("temp","/home/bluleo78/git/sbi-slp-public-plugin/toolchains/public/gdb_build","linux","/home/bluleo78/test/test/unstable") -=begin -#case ARGV[0] -# when "create" then -# pkg_server.create "temp", "unstable", "http://172.21.111.132/pkgserver/", "unstable" -# when "register" then -# #pkg_server.register "/home/taejun/project/sdk-build/test/smart-build-interface_0.19.1_linux.zip", "unstable", "-g" -# pkg_server.register "/home/taejun/project/sdk-build/test/smart-build-interface_0.19.1_linux.zip", "unstable", "" -# when "snapshot" then - pkg_server.snapshot_generate "", "unstable", "", "", "" - when "sync" then - # pkg_server.sync "unstable", "force" - pkg_server.sync "unstable", "" - when "add_distribution" then - pkg_server.add_distribution "test_stable", "stable" - else - puts "First input error : #{ARGV[0]}" -end -=end diff --git a/test/test_pkglist_parser.rb b/test/test_pkglist_parser.rb index c299f7f..767dc8c 100755 --- a/test/test_pkglist_parser.rb +++ b/test/test_pkglist_parser.rb @@ -2,8 +2,7 @@ require '../src/common/parser' require '../src/common/package' -alist = Parser.read_pkg_list "pkg-list" -a_list = alist.values -a_list.each do |l| +alist = Parser.read_multy_pkginfo_from "pkg-list-local" +alist.each do |l| l.print -end +end diff --git a/test/test_server b/test/test_server index cb17835..c3f18a0 100755 --- a/test/test_server +++ b/test/test_server @@ -1,63 +1,66 @@ #!/bin/sh echo "============ remove 1 ==============" -../pkg-svr remove -i temp_local +../pkg-svr remove -n temp_local --force echo "============ remove 2 ==============" -../pkg-svr remove -i temp_remote +../pkg-svr remove -n temp_remote --force echo "============ remove 3 ==============" -../pkg-svr remove -i temp_remote_dup +../pkg-svr remove -n temp_remote_dup --force echo "============ remove 4 ==============" -../pkg-svr remove -i temp_remote_snap +../pkg-svr remove -n temp_remote_snap --force echo "============ create 1 ==============" -../pkg-svr create -i temp_local -d unstable +../pkg-svr create -n temp_local -d unstable echo "============ create 2 ==============" -../pkg-svr create -i temp_remote -d unstable -u http://172.21.17.55/dibs/unstable +../pkg-svr create -n temp_remote -d unstable -u http://172.21.17.55/private/develop echo "============ create 3 ==============" -../pkg-svr create -i temp_remote_dup -d unstable -u temp_remote/unstable +../pkg-svr create -n temp_remote_dup -d unstable -u temp_local/unstable echo "============ add dist 1 ==============" -../pkg-svr add-dist -i temp_local -d stable +../pkg-svr add-dist -n temp_local -d stable echo "============ sync 1 ==============" -../pkg-svr sync -i temp_remote -d unstable +../pkg-svr sync -n temp_remote -d unstable echo "============ sync 2 ==============" -../pkg-svr sync -i temp_remote_dup -d unstable -f +../pkg-svr sync -n temp_remote_dup -d unstable --force echo "============ gen snapshot 1 ==============" -../pkg-svr gen-snapshot -i temp_remote +../pkg-svr gen-snapshot -n temp_remote echo "============ gen snapshot 2 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable +../pkg-svr gen-snapshot -n temp_remote -d unstable echo "============ gen snapshot 3 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -n test +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test echo "============ gen snapshot 4 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -n test2 -b test +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test2 -b test echo "============ gen snapshot 5 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o linux +../pkg-svr gen-snapshot -n temp_remote -d unstable -o linux echo "============ gen snapshot 6 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o windows +../pkg-svr gen-snapshot -n temp_remote -d unstable -o windows echo "============ gen snapshot 7 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o darwin +../pkg-svr gen-snapshot -n temp_remote -d unstable -o darwin echo "============ gen snapshot 8 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o all +../pkg-svr gen-snapshot -n temp_remote -d unstable -o all echo "============ gen snapshot 9 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o all -p test_server_pkg_file/smart-build-interface_1.20.1_linux.zip -n test3 +../pkg-svr gen-snapshot -n temp_remote -d unstable -o all -P test_server_pkg_file/smart-build-interface_1.20.1_linux.zip -s test3 echo "============ create 4 ==============" -../pkg-svr create -i temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/test +../pkg-svr create -n temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/test echo "============ register 1 ==============" -cp test_server_pkg_file/smart-build-interface* ./ -../pkg-svr register -i temp_remote -d unstable -p smart-build-interface_1.20.1_linux.zip -s smart-build-interface_1.20.1.tar.gz -g -echo "============ spkg path 1 ==============" -../pkg-svr spkg-path -i temp_remote -d unstable -s smart-build-interface_1.20.1.tar.gz +cp test_server_pkg_file/smart-build-interface_*_linux.zip ./ +../pkg-svr register -n temp_local -d unstable -P smart-build-interface_1.20.1_linux.zip echo "============ register 2 ==============" -../pkg-svr register -i temp_remote -d unstable -p smart-build-interface_1.20.1_linux.zip -s smart-build-interface_1.20.1.tar.gz -g +cp test_server_pkg_file/smart-build-interface_*_linux.zip ./ +../pkg-svr register -n temp_remote -d unstable -P smart-build-interface_1.20.1_linux.zip --gen echo "============ register 3 ==============" -../pkg-svr register -i temp_remote_dup -d unstable -p ./temp_remote/unstable/binary/smart-build-interface_1.20.1_linux.zip -s ./temp_remote/unstable/source/smart-build-interface_1.20.1.tar.gz -g -t +cp test_server_pkg_file/smart-build-interface_*_linux.zip ./ +../pkg-svr register -n temp_remote_dup -d unstable -P smart-build-interface_1.20.1_linux.zip --gen --test +echo "============ register 4 ==============" +cp test_server_pkg_file/archive.zip ./ +../pkg-svr register -n temp_local -d unstable -A archive.zip echo "============ remove 3 ==============" -../pkg-svr remove-pkg -i temp_local -d unstable -p smart-build-interface +../pkg-svr remove-pkg -n temp_local -d unstable -P smart-build-interface +echo "============ clean 1 ==============" +../pkg-svr clean -n temp_local -d unstable +echo "============ clean 2 ==============" +../pkg-svr clean -n temp_remote -d unstable -s test,test2,test3 echo "============ list 1 ==============" ../pkg-svr list echo "============ list 2 ==============" -../pkg-svr list -i temp_local -#../pkg-svr remove -i temp +../pkg-svr list -n temp_local +#../pkg-svr remove -n temp -#cleanup -rm smart-build-interface_1.20.1_windows.zip -rm smart-build-interface_1.20.1_linux.zip -rm smart-build-interface_1.20.1.tar.gz diff --git a/test/test_server_pkg_file/archive.zip b/test/test_server_pkg_file/archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..140bd05bd7f6f55d6ea86f41bc75c8257b3b8150 GIT binary patch literal 308 zcmWIWW@Zs#U|`^2U~gOFFm;+sLmiN}0f+?{WEcvv(=+qZ^7V2P^D@&?i%UX7I2oAt zimvkq;nE6j21b?_%nS@*qO|uQ7n326%lp4we-2E$I$LSpH?>6pALX*I^{!jFM7JY8 z{|0Nm9GjYP{~5u@%=s+YC;YEmiqY!7-nMkYH;I#TJ&n~j)?NLT`da**z#ECkSGV6@ zbP!(Y%K9>nmF>=%bNo9>6+?Uf?=PRS_|xh$+``Pl%=T=*mJ~eV=WNM)zAwO=kx7mj pmro>s{$XHX1mY!)AQq;-SRwvG^HYE~D;r2XBM^oG=@TFh0{|WrWfA}Y literal 0 HcmV?d00001 diff --git a/test/test_server_pkg_file/smart-build-interface_1.20.1.tar.gz b/test/test_server_pkg_file/smart-build-interface_1.20.1.tar.gz deleted file mode 100644 index 96f13b44a01477675287be92e87f079b4947188b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78 zcmb2|=HS?5XyVVnT%4O&RHBz=GSg9ZZw0IcQ}tpET3 diff --git a/test/test_server_pkg_file/smart-build-interface_1.20.1_linux.zip b/test/test_server_pkg_file/smart-build-interface_1.20.1_linux.zip index 109a1441f2e8f0886379fe411d93b59602e30e58..cc2a217be42c4fc3ef3b9b362f42f5521eac6002 100644 GIT binary patch literal 308 zcmWIWW@Zs#U|`^2*faZ;!_;Xm4Rt`?1|Sw-kYOmuPS4Cs%h$_I%*#wmEiMTS;bdS| zX8Gd}!lf1542&!C$rL}~9qE+#`Bm-m0W{v4Qgb+*#HZ)%GIKFVcZ>s_~UiEc-H z{tecAIW{%p{xgD)ne$n)PxxQC6r01)ZxScxdK#;5th@Rv^|kmrfj1J7uWrA+ z=pek(mGxyDE8Cqj=lFM&Du(v{-(Nmu@u$^ixP_U8neEwrEh%`!&)Jgqd|!Y!Ba<96 pE}uvM{lmb(2*gVoK`cyvu|oWX=BEH}RyL4&Mj#9W(kDP11_0@HW(xoS literal 327 zcmWIWW@Zs#U|`^2U{&*SFz$9po(SY!0Ac|K8HR%F^vt}pe7)Snyv(%J;*!u1P6p;* zN+$jwTw1}+z{v7~nSlXJl=dFvI%FW=^8RPn-^OLv7I%nO3$GOTxUcuN*)6qAo(Fz? z)zhu`Y+zvhd&bTMwMqUPnk^KUFFKpINBo@nBR<`g(uE}lPw&5UyLVkk@{#^?&$nAG zE>KG96uP8bEm5%F%JzZd%n3LD>95y}DqFvWPlrc`=a0k-ro&fjPc*Oef7Q|X>%is* zMbokq=Oiy$nPWKV7gK;YBa<96F3(8-y~x182*gVoK`czKvO>Iy=FI?aRyL4&Mj#9W J(zihz1^^f9Z_NMz diff --git a/tizen-ide/get_ide_sources.sh b/tizen-ide/get_ide_sources.sh index 5f9d7ea..6a04459 100644 --- a/tizen-ide/get_ide_sources.sh +++ b/tizen-ide/get_ide_sources.sh @@ -16,7 +16,11 @@ GIT_LIST=" /sdk/ide/common-eplugin /sdk/ide/eventinjector-eplugin /sdk/ide/nativecommon-eplugin +/sdk/ide/nativeappcommon-eplugin /sdk/ide/nativeapp-eplugin +/sdk/ide/nativecpp-eplugin +/sdk/ide/nativecpp-ext-eplugin +/sdk/ide/native-sample /sdk/ide/nativeplatform-eplugin /sdk/ide/unittest-eplugin /sdk/ide/native-gui-builder-eplugin @@ -185,6 +189,33 @@ function git_checkout_all() { cd ${START_PATH} } +## Command git +function git_command() { + GIT_PATH=$1 + GIT_NAME=${GIT_PATH##*/} + + ## ARG1 : + cd ${ARG1}/${GIT_NAME} + isError "Found git directory ( ${ARG1}/${GIT_NAME} )" + git ${SCRIPT_OPERATION} + isError "Pulled ${GIT_NAME}" +} + +## Command git all +function git_command_all() { + draw_line; echo "Git ${SCRIPT_OPERATION}"; draw_line + + cd ${ARG1} + isError "Checked source directory ( ${ARG1} )" + + for GIT_EACH in ${GIT_LIST} + do + git_command ${GIT_EACH} + done + + cd ${START_PATH} +} + ############################################################### ## Begin script ############################################################### @@ -228,7 +259,12 @@ case ${SCRIPT_OPERATION} in ## process default *) - usage + if [ "$#" == 1 ]; then + ARG1=$(pwd) + git_command_all + else + usage + fi ;; esac diff --git a/upgrade b/upgrade new file mode 100644 index 0000000..b93eefe --- /dev/null +++ b/upgrade @@ -0,0 +1,256 @@ +#!/usr/bin/ruby +=begin + + upgrade + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang +Sungmin Kim + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'optparse' +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/build_server" +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/pkg_server" +require "utils.rb" +require "log.rb" +require "BuildServerController" +require "packageServerLog" + +def option_error_check( options ) + if options[:start] then + if options[:locate].nil? or options[:type].nil? or options[:name].nil? then + raise ArgumentError, "upgrade -l -S -t -n -p " + "\n" + end + else + if options[:locate].nil? or options[:url].nil? then + raise ArgumentError, "upgrade -u [-l ]" + "\n" + end + end +end + +def option_parse + options = {} + banner = "DIBS upgrade service command-line tool." + "\n" \ + + "\n" + "Usage: upgrade -u [-l ]" + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + opts.banner = banner + + options[:locate] = File.dirname(__FILE__) + opts.on( '-l', '--locate ', 'located dibs path' ) do|locate| + options[:locate] = locate + end + + opts.on( '-u', '--url ', 'package server url: http://127.0.0.1/dibs/unstable' ) do|url| + options[:url] = url + end + + opts.on( '-I', '--install', 'install, internal option' ) do + options[:install] = true + end + + opts.on( '-S', '--start', 'start server option' ) do + options[:start] = true + end + + opts.on( '-t', '--type ', 'sever type : BUILDSERVER or PACKAGESERVER' ) do|type| + options[:type] = type + end + + opts.on( '-n', '--name ', 'build server name or package server name' ) do|name| + options[:name] = name + end + + options[:port] = 2222 + opts.on( '-p', '--port ', 'server port number: 2224' ) do|port| + options[:port] = port.strip.to_i + end + + opts.on( '-h', '--help', 'display this information' ) do + puts opts.banner + puts " -l, --locate , located dibs path" + puts " -u, --url , package server url: http://127.0.0.1/dibs/unstable" + exit + end + end + + optparse.parse! + + option_error_check options + + return options +end + +#option parsing +begin + option = option_parse +rescue => e + puts e.message + exit 0 +end + +# Upgrade DIBS +begin + install_opt = option[:install] + sub_cmd = option[:sub_cmd] + dibs_path = option[:locate] + pkg_svr_url= option[:url] + start_opt = option[:start] + svr_type = option[:type] + svr_name = option[:name] + svr_port = option[:port] + + DIBS_PKG_NAME = "dibs" + BACKUP_ROOT = Utils::HOME + "/.build_tools/backup" + PREV_VER_PATH = BACKUP_ROOT + "/prev_ver" + NEW_VER_PATH = BACKUP_ROOT + "/new_ver" + UPGRADE_CMD = "#{PREV_VER_PATH}/upgrade" + BUILD_CONFIG_ROOT = "#{Utils::HOME}/.build_tools/build_server/#{svr_name}" + BUILD_FRIENDS_FILE = "#{BUILD_CONFIG_ROOT}/friends" + + if not File.exist? BACKUP_ROOT then FileUtils.mkdir_p(BACKUP_ROOT) end + log = PackageServerLog.new( "#{BACKUP_ROOT}/log" ) + + if not install_opt then + puts "" + log.info("Upgrade Start...", Log::LV_USER) + + # Backup current dibs + if File.exist? PREV_VER_PATH then FileUtils.rm_rf(PREV_VER_PATH) end + if File.exist? NEW_VER_PATH then FileUtils.rm_rf(NEW_VER_PATH) end + FileUtils.mkdir_p(PREV_VER_PATH) + FileUtils.mkdir_p(NEW_VER_PATH) + FileUtils.cp_r("#{dibs_path}/.", PREV_VER_PATH, :preserve => true) + log.info("Backup DIBS [#{dibs_path}] -> [#{PREV_VER_PATH}]", Log::LV_USER) + + # Run Upgrade + if start_opt and svr_type.eql? "BUILDSERVER" then + cmd = "#{UPGRADE_CMD} -I -l #{dibs_path} -S -t #{svr_type} -n #{svr_name} -p #{svr_port}" + else + cmd = "#{UPGRADE_CMD} -I -l #{dibs_path} -u #{pkg_svr_url}" + end + + cmd = Utils.execute_shell_generate(cmd) + Utils.spawn(cmd) + + else + # Get SERVER INFORMATION + if start_opt and svr_type.eql? "BUILDSERVER" then + build_server = BuildServerController.get_server(svr_name) + pkg_svr_url = build_server.pkgserver_url + log.info("Build server : [#{svr_name}][#{svr_port}]", Log::LV_USER) + end + log.info("Package Server : [#{pkg_svr_url}]", Log::LV_USER) + log.info("DIBS Path : [#{dibs_path}]", Log::LV_USER) + + # Download DIBS Package + client = Client.new( pkg_svr_url, NEW_VER_PATH, log) + client.update() + client.install( DIBS_PKG_NAME, Utils::HOST_OS, true, true) + + # Copy Current path + if File.exist? "#{dibs_path}" then + FileUtils.rm_rf("#{dibs_path}") + #FileUtils.mkdir_p("#{dibs_path}") + end + if File.exist? "#{NEW_VER_PATH}/tools/dibs" then + FileUtils.cp_r("#{NEW_VER_PATH}/tools/dibs/.", "#{dibs_path}", :preserve => true) + else + log.error("Not installed package error.", Log::LV_USER) + exit(1) + end + + # Execute start command + if start_opt + if svr_type.eql? "BUILDSERVER" then + # get friends server information + if File.exist? BUILD_FRIENDS_FILE then + File.open( BUILD_FRIENDS_FILE, "r" ) do |f| + f.each_line do |l| + if l.split(",").count < 2 then + next + end + + ip = l.split(",")[0].strip + port = l.split(",")[1].strip + + build_client = BuildCommClient.create( ip, port ) + if build_client.nil? then + log.info("Friend Server #{ip}:#{port} is not running!", Log::LV_USER) + next + end + + # send request + if build_client.send "UPGRADE|#{build_server.password}" then + # recevie & print + mismatched = false + result = build_client.read_lines do |l| + log.error(l, Log::LV_USER) + if l.include? "Password mismatched!" then + mismatched = true + end + end +=begin + if result and not mismatched then + log.info("Friend Server #{ip}:#{port} upgrade failed!", Log::LV_USER) + else + log.info("Friend Server #{ip}:#{port} upgrade requested!", Log::LV_USER) + end +=end + end + + # terminate + build_client.terminate + end + end + else + log.info("No Friend Server.", Log::LV_USER) + end + + # Start Build server + cmd = Utils.execute_shell_generate("#{dibs_path}/build-svr start -n #{svr_name} -p #{svr_port}") + Utils.spawn(cmd) + + log.info("Upgrade Complete", Log::LV_USER) + log.info("Start Build server [#{cmd}]", Log::LV_USER) + + else # PACKAGE SERVER + # Start Build server + cmd = Utils.execute_shell_generate("#{dibs_path}/pkg-svr start -n #{svr_name} -p #{svr_port}") + Utils.spawn(cmd) + + log.info("Upgrade Complete", Log::LV_USER) + log.info("Start Package server [#{cmd}]", Log::LV_USER) + end + else + log.info("Upgrade Complete", Log::LV_USER) + end + end +rescue => e + log.error(e.message, Log::LV_USER) + #puts e.message +end + -- 2.34.1