Skip to content

Commit 2a5162a

Browse files
committed
Implement symlink logic for windows hosts
1 parent 04dee9d commit 2a5162a

10 files changed

+176
-38
lines changed

.travis.yml

+13-13
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ matrix:
2020
#before_install: gem install bundler -v 1.15.4
2121
script:
2222
- g++ -v
23-
- bundle install
24-
- bundle exec rubocop --version
25-
- bundle exec rubocop -D .
26-
- bundle exec rspec --backtrace
27-
- cd SampleProjects/TestSomething
28-
- bundle install
29-
- bundle exec arduino_ci.rb
30-
- cd ../NetworkLib
31-
- cd scripts
32-
- bash -x ./install.sh
33-
- cd ..
34-
- bundle install
35-
- bundle exec arduino_ci.rb
23+
# - bundle install
24+
# - bundle exec rubocop --version
25+
# - bundle exec rubocop -D .
26+
# - bundle exec rspec --backtrace
27+
# - cd SampleProjects/TestSomething
28+
# - bundle install
29+
# - bundle exec arduino_ci.rb
30+
# - cd ../NetworkLib
31+
# - cd scripts
32+
# - bash -x ./install.sh
33+
# - cd ..
34+
# - bundle install
35+
# - bundle exec arduino_ci.rb

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111
- Explicit checks for attemping to test `arduino_ci` itself as if it were a library, resolving a minor annoyance to this developer.
1212
- Code coverage tooling
1313
- Explicit check and warning for library directory names that do not match our guess of what the library should/would be called
14+
- Symlink tests for `Host`
1415

1516
### Changed
1617
- Arduino backend is now `arduino-cli` version `0.13.0`

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

2-
# ArduinoCI Ruby gem (`arduino_ci`)
3-
[![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci)
2+
# ArduinoCI Ruby gem (`arduino_ci`)
3+
[![Gem Version](https://badge.fury.io/rb/arduino_ci.svg)](https://rubygems.org/gems/arduino_ci)
44
[![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/arduino_ci/0.4.0)
55
[![Gitter](https://badges.gitter.im/Arduino-CI/arduino_ci.svg)](https://gitter.im/Arduino-CI/arduino_ci?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
66

@@ -36,6 +36,9 @@ For a bare-bones example that you can copy from, see [SampleProjects/DoSomething
3636

3737
The complete set of C++ unit tests for the `arduino_ci` library itself are in the [SampleProjects/TestSomething](SampleProjects/TestSomething) project. The [test files](SampleProjects/TestSomething/test/) are named after the type of feature being tested.
3838

39+
> Arduino expects all libraries to be in a specific `Arduino/libraries` directory on your system. If your library is elsewhere, `arduino_ci` will _automatically_ create a symbolic link in the `libraries` directory that points to the directory of the project being tested. This simplifieds working with project dependencies, but **it can have unintended consequences on Windows systems** because [in some cases deleting a folder that contains a symbolic link to another folder can cause the _entire linked folder_ to be removed instead of just the link itself](https://superuser.com/a/306618).
40+
>
41+
> If you use a Windows system **it is recommended that you only run `arduino_ci` from project directories that are already inside the `libraries` directory**
3942
4043
### You Need Ruby and Bundler
4144

appveyor.yml

+15-12
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ before_test:
1919
test_script:
2020
# https://help.appveyor.com/discussions/problems/5170-progresspreference-not-works-always-shown-preparing-modules-for-first-use-in-stderr
2121
- ps: $ProgressPreference = "SilentlyContinue"
22-
- bundle exec rubocop --version
23-
- bundle exec rubocop -D .
24-
- bundle exec rspec --backtrace
25-
- cd SampleProjects\TestSomething
26-
- bundle install
27-
- bundle exec arduino_ci.rb
28-
- cd ../NetworkLib
29-
- cd scripts
30-
- install.sh
31-
- cd ..
32-
- bundle install
33-
- bundle exec arduino_ci.rb
22+
# - bundle exec rubocop --version
23+
# - bundle exec rubocop -D .
24+
- bundle exec rspec spec/host_spec.rb
25+
- bundle exec rspec spec/ci_config_spec.rb
26+
- bundle exec rspec spec/arduino_backend_spec.rb
27+
- bundle exec rspec spec/cpp_library_spec.rb
28+
# - cd SampleProjects\TestSomething
29+
# - bundle install
30+
# - bundle exec arduino_ci.rb
31+
# - cd ../NetworkLib
32+
# - cd scripts
33+
# - install.sh
34+
# - cd ..
35+
# - bundle install
36+
# - bundle exec arduino_ci.rb

exe/arduino_ci.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def file_is_hidden_somewhere?(path)
157157
# print out some files
158158
def display_files(pathname)
159159
# `find` doesn't follow symlinks, so we should instead
160-
realpath = pathname.symlink? ? pathname.readlink : pathname
160+
realpath = Host.symlink?(pathname) ? Host.readlink(pathname) : pathname
161161

162162
# suppress directories and dotfile-based things
163163
all_files = realpath.find.select(&:file?)

lib/arduino_ci/arduino_backend.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,11 @@ def install_local_library(path)
196196

197197
uhoh = "There is already a library '#{library_name}' in the library directory (#{destination_path})"
198198
# maybe it's a symlink? that would be OK
199-
if destination_path.symlink?
200-
return cpp_library if destination_path.readlink == src_path
199+
if Host.symlink?(destination_path)
200+
current_destination_target = Host.readlink(destination_path)
201+
return cpp_library if current_destination_target == src_path
201202

202-
@last_msg = "#{uhoh} and it's not symlinked to #{src_path}"
203+
@last_msg = "#{uhoh} and it's symlinked to #{current_destination_target} (expected #{src_path})"
203204
return nil
204205
end
205206

lib/arduino_ci/host.rb

+59-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ module ArduinoCI
66

77
# Tools for interacting with the host machine
88
class Host
9+
# TODO: this came from https://stackoverflow.com/a/22716582/2063546
10+
# and I'm not sure if it can be replaced by self.os == :windows
11+
WINDOWS_VARIANT_REGEX = /mswin32|cygwin|mingw|bccwin/
12+
13+
# e.g. 11/27/2020 01:02 AM <SYMLINKD> ExcludeSomething [C:\projects\arduino-ci\SampleProjects\ExcludeSomething]
14+
DIR_SYMLINK_REGEX = %r{\d+/\d+/\d+\s+[^<]+<SYMLINKD?>\s+(.*) \[([^\]]+)\]}
15+
916
# Cross-platform way of finding an executable in the $PATH.
1017
# via https://stackoverflow.com/a/5471032/2063546
1118
# which('ruby') #=> /usr/bin/ruby
@@ -38,21 +45,69 @@ def self.os
3845
return :windows if OS.windows?
3946
end
4047

48+
# Cross-platform symlinking
4149
# if on windows, call mklink, else self.symlink
4250
# @param [Pathname] old_path
4351
# @param [Pathname] new_path
4452
def self.symlink(old_path, new_path)
45-
return FileUtils.ln_s(old_path.to_s, new_path.to_s) unless RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
53+
# we would prefer `new_path.make_symlink(old_path)` but "symlink function is unimplemented on this machine" with windows
54+
return new_path.make_symlink(old_path) unless needs_symlink_hack?
4655

47-
# https://stackoverflow.com/a/22716582/2063546
56+
# via https://stackoverflow.com/a/22716582/2063546
4857
# windows mklink syntax is reverse of unix ln -s
4958
# windows mklink is built into cmd.exe
5059
# vulnerable to command injection, but okay because this is a hack to make a cli tool work.
51-
orp = old_path.realpath.to_s.tr("/", "\\") # HACK DUE TO REALPATH BUG where it
52-
np = new_path.to_s.tr("/", "\\") # still joins windows paths with '/'
60+
orp = pathname_to_windows(old_path.realpath)
61+
np = pathname_to_windows(new_path)
5362

5463
_stdout, _stderr, exitstatus = Open3.capture3('cmd.exe', "/C mklink /D #{np} #{orp}")
5564
exitstatus.success?
5665
end
66+
67+
# Hack for "realpath" which on windows joins paths with slashes instead of backslashes
68+
# @param path [Pathname] the path to render
69+
# @return [String] A path that will work on windows
70+
def self.pathname_to_windows(path)
71+
path.to_s.tr("/", "\\")
72+
end
73+
74+
# Hack for "realpath" which on windows joins paths with slashes instead of backslashes
75+
# @param str [String] the windows path
76+
# @return [Pathname] A path that will be recognized by pathname
77+
def self.windows_to_pathname(str)
78+
Pathname.new(str.tr("\\", "/"))
79+
end
80+
81+
# Whether this OS requires a hack for symlinks
82+
# @return [bool]
83+
def self.needs_symlink_hack?
84+
RUBY_PLATFORM =~ WINDOWS_VARIANT_REGEX
85+
end
86+
87+
# Cross-platform is-this-a-symlink function
88+
# @param [Pathname] path
89+
# @return [bool] Whether the file is a symlink
90+
def self.symlink?(path)
91+
return path.symlink? unless needs_symlink_hack?
92+
93+
!readlink(path).nil?
94+
end
95+
96+
# Cross-platform "read link" function
97+
# @param [Pathname] path
98+
# @return [Pathname] the link target
99+
def self.readlink(path)
100+
return path.readlink unless needs_symlink_hack?
101+
102+
the_dir = pathname_to_windows(path.parent)
103+
the_file = path.basename.to_s
104+
105+
stdout, _stderr, _exitstatus = Open3.capture3('cmd.exe', "/c dir /al #{the_dir}")
106+
symlinks = stdout.lines.map { |l| DIR_SYMLINK_REGEX.match(l) }.compact
107+
our_link = symlinks.find { |m| m[1] == the_file }
108+
return nil if our_link.nil?
109+
110+
windows_to_pathname(our_link[2])
111+
end
57112
end
58113
end

spec/ci_config_spec.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,27 @@
171171
expect(cpp_lib_path.exist?).to be(true)
172172
expect(@cpp_library).to_not be(nil)
173173
expect(@cpp_library.path.exist?).to be(true)
174-
expect(@cpp_library.test_files.map { |f| File.basename(f) }).to match_array([
174+
expect(@cpp_library.test_files.map(&:basename).map(&:to_s)).to match_array([
175175
"sam-squamsh.cpp",
176176
"yes-good.cpp",
177177
"mars.cpp"
178178
])
179179
end
180180

181181
it "filters that set of files" do
182+
expect(cpp_lib_path.exist?).to be(true)
183+
expect(@cpp_library).to_not be(nil)
184+
expect(@cpp_library.test_files.map(&:basename).map(&:to_s)).to match_array([
185+
"sam-squamsh.cpp",
186+
"yes-good.cpp",
187+
"mars.cpp"
188+
])
189+
182190
override_file = File.join(File.dirname(__FILE__), "yaml", "o1.yaml")
183191
combined_config = ArduinoCI::CIConfig.default.with_override(override_file)
184-
expect(combined_config.allowable_unittest_files(@cpp_library.test_files).map { |f| File.basename(f) }).to match_array([
192+
expect(combined_config.unittest_info[:testfiles][:select]).to match_array(["*-*.*"])
193+
expect(combined_config.unittest_info[:testfiles][:reject]).to match_array(["sam-squamsh.*"])
194+
expect(combined_config.allowable_unittest_files(@cpp_library.test_files).map(&:basename).map(&:to_s)).to match_array([
185195
"yes-good.cpp",
186196
])
187197
end

spec/fake_lib_dir.rb

+13-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def initialize
1818

1919
# designed to be called by rspec's "around" function
2020
def in_pristine_fake_libraries_dir(example)
21-
Dir.mktmpdir do |d|
21+
d = Dir.mktmpdir
22+
begin
2223
# write a yaml file containing the current directory
2324
dummy_config = { "directories" => { "user" => d.to_s } }
2425
@arduino_dir = Pathname.new(d)
@@ -37,6 +38,17 @@ def in_pristine_fake_libraries_dir(example)
3738
# cool, already done
3839
end
3940
end
41+
ensure
42+
if ArduinoCI::Host.needs_symlink_hack?
43+
stdout, stderr, exitstatus = Open3.capture3('cmd.exe', "/c rmdir /s /q #{ArduinoCI::Host.pathname_to_windows(d)}")
44+
unless exitstatus.success?
45+
puts "====== rmdir of #{d} failed"
46+
puts stdout
47+
puts stderr
48+
end
49+
else
50+
FileUtils.remove_entry(d)
51+
end
4052
end
4153
end
4254

spec/host_spec.rb

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require "spec_helper"
2+
require 'tmpdir'
3+
4+
5+
def idempotent_delete(path)
6+
path.delete
7+
rescue Errno::ENOENT
8+
end
9+
10+
# creates a dir at <path> then deletes it after block executes
11+
# this will DESTROY any existing entry at that location in the filesystem
12+
def with_tmpdir(path)
13+
begin
14+
idempotent_delete(path)
15+
path.mkpath
16+
yield
17+
ensure
18+
idempotent_delete(path)
19+
end
20+
end
21+
22+
23+
RSpec.describe ArduinoCI::Host do
24+
next if skip_ruby_tests
25+
26+
context "symlinks" do
27+
it "creates symlinks that we agree are symlinks" do
28+
our_dir = Pathname.new(__dir__)
29+
foo_dir = our_dir + "foo_dir"
30+
bar_dir = our_dir + "bar_dir"
31+
32+
with_tmpdir(foo_dir) do
33+
foo_dir.unlink # we just want to place something at this location
34+
expect(foo_dir.exist?).to be_falsey
35+
36+
with_tmpdir(bar_dir) do
37+
expect(bar_dir.exist?).to be_truthy
38+
expect(bar_dir.symlink?).to be_falsey
39+
40+
ArduinoCI::Host.symlink(bar_dir, foo_dir)
41+
expect(ArduinoCI::Host.symlink?(bar_dir)).to be_falsey
42+
expect(ArduinoCI::Host.symlink?(foo_dir)).to be_truthy
43+
expect(ArduinoCI::Host.readlink(foo_dir).realpath).to eq(bar_dir.realpath)
44+
end
45+
end
46+
47+
expect(foo_dir.exist?).to be_falsey
48+
expect(bar_dir.exist?).to be_falsey
49+
50+
end
51+
end
52+
53+
end

0 commit comments

Comments
 (0)